@dumbmice/pi-deepseek-websearch 0.1.2 → 0.1.4

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/AGENTS.md ADDED
@@ -0,0 +1,156 @@
1
+ # Working on this repository
2
+
3
+ Instructions for anyone — human or coding agent — changing
4
+ `@dumbmice/pi-deepseek-websearch`. For what the extension does and how to install it, read
5
+ [README.md](README.md).
6
+
7
+ ## Layout
8
+
9
+ ```
10
+ src/index.ts the extension: two tool registrations, transport, formatting, guards
11
+ test/index.test.ts node:test suite; global fetch is stubbed, nothing touches the network
12
+ docs/docs-alignment.md every client-side deviation from DeepSeek's published API contract
13
+ README.md user-facing documentation
14
+ AGENTS.md this file
15
+ CONTRIBUTING.md four-line pointer to this file (GitHub convention)
16
+ CHANGELOG.md Keep a Changelog format, released and unreleased entries
17
+ package.json scripts, engines, the files allow-list, pi.extensions entry point
18
+ tsconfig.json the strict / noEmit settings used by typecheck
19
+ .github/workflows/ test.yml (Node 22 and 24) and publish.yml (trusted publishing)
20
+ ```
21
+
22
+ ## Requirements and commands
23
+
24
+ Node >= 22.6 (`engines`; the entry point is TypeScript and the test runner uses
25
+ `--experimental-strip-types`). devDependencies stay limited to `typescript` and `@types/node`; the
26
+ publish workflow installs with `npm ci`, the test matrix with `npm install`.
27
+
28
+ ```bash
29
+ npm install # devDependencies only: typescript, @types/node
30
+ npm test # node:test, mocked HTTP, no key required
31
+ npm run typecheck # tsc --noEmit, strict
32
+ npm run pack:check # tarball contents: package.json, src, docs, README, AGENTS, CONTRIBUTING, LICENSE, CHANGELOG
33
+ ```
34
+
35
+ ## Design decisions worth preserving
36
+
37
+ These are the invariants the suite protects. Changing one means changing its test, any affected
38
+ README text, the matching `docs/docs-alignment.md` entry (or marking that item silent), and adding a
39
+ `CHANGELOG.md` line — all in the same commit.
40
+
41
+ - **No runtime dependencies.** The only import is `node:net`. Everything else is the platform. Do not
42
+ add a dependency for argument validation, HTTP, or markdown cleanup.
43
+ - **Plain JSON Schema, not TypeBox.** The `parameters` are literal JSON Schema objects rather than
44
+ TypeBox builders. That keeps the schema out of the dependency graph, which is the point of the
45
+ no-runtime-dependencies rule. Do not replace them with `Type.Object` imports.
46
+ - **A fitting query is never rewritten.** `fitQuery` returns the caller's text untouched when it fits,
47
+ apart from stripped leading/trailing whitespace. Only an over-long query is tokenised and trimmed.
48
+ - **Trimming is bounded and reported.** Shed filters (recency, language, domains) first, then trailing
49
+ words, then a code-point tail cut; drop a dangling operator or unmatched quote; record every removal
50
+ in `dropped`. Whitespace stripped from `query`, `ori_question` and `localization` before use is
51
+ silent (register item 3).
52
+ - **Cost follows the raw response.** `estimatedCostUsd` is keyed to `rawResults.length`, not to what
53
+ survives client-side filtering, because DeepSeek bills a non-empty result set.
54
+ - **A malformed 200 is an error.** A body without a `results` array is rejected; an empty array is a
55
+ legitimate "no results".
56
+ - **Abort is not failure.** The caller's abort reason is preserved on every path, including retry
57
+ backoff, and an already-aborted signal issues no request.
58
+ - **Never trust response field types.** `formatResult` degrades per field instead of failing the call.
59
+ - **Code points, not UTF-16 units.** Every documented length cap (`128`, `8192`, `max_chars`, the
60
+ 4,000-character result body) uses `Array.from`.
61
+ - **Endpoint and credential precedence is deliberate.** Key: pi's `deepseek` provider, then
62
+ `DEEPSEEK_API_KEY`. Base URL: `PI_DEEPSEEK_BASE_URL`, then `DEEPSEEK_SEARCH_BASE_URL`, then
63
+ `https://api.deepseek.com`, with a trailing slash or `/v1` suffix stripped. The bare
64
+ `DEEPSEEK_BASE_URL` is ignored on purpose — other tools point it at an OpenAI-compatible `/v1`
65
+ root. Pinned by the `baseUrl` test.
66
+ - **The private-address guard is a policy, not a protocol rule.** It normalises trailing dots and
67
+ decodes IPv4 embedded in IPv6 (`::ffff:127.0.0.1`, `::7f00:1`, NAT64 `64:ff9b::…`). It performs no
68
+ DNS lookup, so a public hostname resolving to a private address is intentionally not caught.
69
+ - **No secrets in output.** The API key is redacted from upstream error text and network failures, and
70
+ error bodies are sliced before display. The redactor only substitutes keys longer than 8 characters,
71
+ which every real DeepSeek key is.
72
+
73
+ ## Adding or changing a tool
74
+
75
+ Register the tool with `pi.registerTool({ … })` inside the default export; pi finds the entry point
76
+ through `package.json` → `pi.extensions`.
77
+
78
+ - `name`, `label`, `description`, `promptSnippet` are model-facing. State when to use the tool and what
79
+ comes back, and keep the parameter descriptions truthful about limits.
80
+ - `parameters` is literal JSON Schema (`type: "object"`, `required`, `additionalProperties: false`).
81
+ - `execute(toolCallId, params, signal, onUpdate, ctx)` reads credentials through
82
+ `ctx.modelRegistry`, passes the abort signal into `postDeepSeek`, and returns
83
+ `{ content: [{ type: "text", text }], details }`.
84
+ - `details` carries machine-readable metadata only — counts, what was trimmed or discarded, cost. Do
85
+ not repeat the rendered text, and keep the object JSON-serialisable (no `undefined`, `NaN` or
86
+ `Infinity`).
87
+ - Then update the README (parameters and behaviour), `docs/docs-alignment.md` if behaviour deviates,
88
+ and `CHANGELOG.md` under `[Unreleased]`. Keep `package.json` `files` and `pi.extensions` in step.
89
+
90
+ ## Tests
91
+
92
+ `test/index.test.ts` stubs `globalThis.fetch` (`harness()` and `stubFetch()`) and asserts behaviour,
93
+ not implementation trivia. Restore the stub in a `finally` block. When fixing a bug, add the failing
94
+ case first; several rounds of review found real defects this way (operator-tail trimming, quote
95
+ parity, abort reasons, malformed bodies). Run `npm test` before every commit; CI repeats it on Node 22
96
+ and 24 together with `npm run typecheck` and `npm run pack:check`.
97
+
98
+ ### Try a change locally
99
+
100
+ There is no build step: pi loads `src/index.ts` through `package.json` → `pi.extensions`.
101
+
102
+ ```bash
103
+ pi -e /absolute/path/to/pi-deepseek-websearch # one session, straight from the working copy
104
+ ```
105
+
106
+ A live call needs a real key and is billed $0.00075 per successful request; one search and one fetch
107
+ is enough to check rendering. `npm test` needs neither key nor network.
108
+
109
+ ## Documentation conventions
110
+
111
+ - `README.md` is for users of the extension: what the tools do, when to use them, parameters, cost,
112
+ observable behaviour, troubleshooting. No implementation inventories, no CI or release steps.
113
+ - `docs/docs-alignment.md` is the register of deliberate differences from DeepSeek's API. Add an item
114
+ when behaviour deviates and pin it with a test wherever the behaviour is observable; items that
115
+ produce no output signal are marked _(silent)_. `DOCS_SNAPSHOT` appears in the `src/index.ts` header
116
+ and constant and at the top of the register — keep them in step.
117
+ - `CHANGELOG.md` follows Keep a Changelog. Put unreleased work under `[Unreleased]` with
118
+ Added/Changed/Fixed headings; add the version heading when the release is tagged.
119
+ - `AGENTS.md` (this file) holds contributor, architecture and release material.
120
+
121
+ ## Release process
122
+
123
+ Publishing uses npm [trusted publishing](https://docs.npmjs.com/trusted-publishers): the workflow
124
+ exchanges a GitHub OIDC token for a short-lived credential, so the repository holds no `NPM_TOKEN`
125
+ and no long-lived write token is needed. Because the repository is public, npm attaches a provenance
126
+ attestation automatically — no `--provenance` flag.
127
+
128
+ ```bash
129
+ npm version patch --no-git-tag-version # or minor/major; the working tree must be clean
130
+ VERSION="$(node -p "require('./package.json').version")"
131
+ # add the CHANGELOG.md entry for this version, then:
132
+ git add -A # include any new docs; -am skips untracked files
133
+ git commit -m "chore: release v$VERSION"
134
+ git tag -a "v$VERSION" -m "v$VERSION"
135
+ git push origin main --follow-tags
136
+ ```
137
+
138
+ `--follow-tags` pushes only **annotated** tags, which is why the recipe uses `git tag -a`; a
139
+ lightweight tag would stay local and publish nothing. `.github/workflows/publish.yml` runs the suite
140
+ and the typecheck, refuses to publish when the tag and `package.json` version disagree, then publishes.
141
+ A manual `workflow_dispatch` run skips the tag guard and publishes the selected ref as-is — use it only
142
+ to retry a failed tag build.
143
+
144
+ Optionally, after a successful trusted publish, set the package's *Publishing access* to "Require
145
+ two-factor authentication and disallow tokens": trusted publishing keeps working, token publishing
146
+ stops.
147
+
148
+ ## Commits and security
149
+
150
+ - Commits follow Conventional Commits (`feat:`, `fix:`, `docs:`, `ci:`, `chore:`); release commits are
151
+ `chore: release vX.Y.Z`.
152
+ - Style: tabs and double quotes, no formatter or linter in the repository. Do not reformat untouched
153
+ lines.
154
+ - Report vulnerabilities through GitHub's private advisory form (Security → Report a vulnerability),
155
+ not a public issue.
156
+ - Contributions are accepted under the MIT license (see [LICENSE](LICENSE)).
package/CHANGELOG.md CHANGED
@@ -1,70 +1,80 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.2 — 2026-10-30
3
+ All notable changes to this extension are documented in this file. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
6
 
5
- - README: document the release path in full — trusted publishing via GitHub OIDC, no
6
- `NPM_TOKEN`, automatic provenance, the tag/version guard, and the option to disallow
7
- token publishing once trusted publishing is proven.
7
+ ## [Unreleased]
8
8
 
9
- ## 0.1.1 — 2026-10-30
9
+ ## [0.1.4] - 2026-10-30
10
10
 
11
- Documentation-only release: npm snapshots the README at publish time, so this
12
- republishes the corrected text.
11
+ ### Added
13
12
 
14
- - Install command moved directly under the intro and no longer says "once the
15
- package is published".
16
- - The sample-output block no longer overflows horizontally; the per-result
17
- fields it used to cram into a comment are described in the prose below it.
18
- - README badges: npm version, license, Node engine, CI status, and a
19
- "coexists with pi-web-access" badge (a downloads badge is deferred until the
20
- package has statistics).
13
+ - Test coverage for previously unpinned behaviour: the API request is sent with `redirect: "error"` so
14
+ a redirect is not followed (register item 15), upstream error text is sliced to 400 characters
15
+ (register item 22), Fetch output truncation and the `max_chars` upper clamp, the `retry-after` delay
16
+ and its 5 second cap, and rejection of domain filters that would rewrite the query structure.
21
17
 
22
- ## 0.1.0 — 2026-10-30
18
+ ### Changed
23
19
 
24
- First release. Package name: `@dumbmice/pi-deepseek-websearch` (web search and page fetch over
25
- DeepSeek's Search & Fetch APIs); published scoped because npm treats the unscoped name as too
26
- similar to the existing `pi-deepseek-web-search`.
20
+ - Documentation overhaul. The README is now strictly for users of the extension: what the tools do,
21
+ when to use them, parameters, cost, observable behaviour and troubleshooting. The list of deliberate
22
+ differences from DeepSeek's API moved to `docs/docs-alignment.md`. Contributor, architecture and
23
+ release material moved to `AGENTS.md` (`CONTRIBUTING.md` now points there), including the corrected
24
+ release recipe with annotated tags. This changelog moved to the Keep a Changelog format.
25
+ - Package metadata: the npm description and keywords were rewritten, `files` now also ships `docs/`,
26
+ `AGENTS.md` and `CONTRIBUTING.md` so the deviation register and contributor guide travel with the
27
+ package, `author.name` is `DumbMice`, and the `LICENSE` copyright holder changed to `DumbMice` to
28
+ match.
27
29
 
28
- **Tools**
30
+ ### Fixed
29
31
 
30
- - `deepseek_search` and `deepseek_fetch` over `POST /v1/tools/search` and `POST /v1/tools/fetch`.
31
- - Blank/whitespace-only `query` is rejected locally with a clear message.
32
- - Search parameters: `count` 1–50, `ori_question`, `need_content`, `localization`, `allowed_domains`, `blocked_domains`, `lang`, `recency`.
33
- - Fetch parameters: `url`, `max_chars` (500–200,000, default 20,000).
34
- - Result metadata: `date`, `authority` (`-1` → *unknown*), `rerank_score` (≥1.5 annotated), `site_name`; `site_icon` available in `details`.
35
- - `details` reports the final query, anything trimmed, discarded non-http(s) results, and `estimatedCostUsd`.
32
+ - The contributor release recipe paired `git tag` with `git push --follow-tags`, which pushes only
33
+ annotated tags, so the documented steps would not have published anything. The recipe now creates an
34
+ annotated tag after the version bump.
36
35
 
37
- **Query handling**
36
+ ## [0.1.3] - 2026-10-30
38
37
 
39
- - A fitting query is sent exactly as written, spacing included.
40
- - An over-length query is trimmed instead of rejected: recency → language → domain filters → trailing words, with a final code-point cut only if a single token exceeds the budget. Every removal is reported.
41
- - Trimming is token-aware: quoted phrases stay whole and a trailing operator is never left dangling.
42
- - `ori_question` longer than 128 code points is cut, and the cut is reported in the output and `details`; the tool-parameter schema has no `maxLength`, so the code-point cut is authoritative.
43
- - `details.requestedCount` reports the pre-clamp `count` ask.
44
- - Unknown `recency` values and non-IP `localization` values are rejected with an actionable message.
38
+ ### Changed
45
39
 
46
- **Transport**
40
+ - README kept user-facing: development and release instructions moved to `CONTRIBUTING.md`, with a
41
+ single pointer left behind.
47
42
 
48
- - Retries for `408`, `429`, any `5xx`, network failures and timeouts; 3 attempts; `retry-after` honoured in seconds or HTTP-date form, capped at 5 s.
49
- - Documented remedies for `401`/`402`/`403`/`404`/`413`/`429`; all other `4xx` fail immediately.
50
- - 30 s search / 60 s fetch timeouts; the caller's abort reason is preserved on every path, including during retry backoff.
51
- - A 200 search response without a `results` array is rejected as an invalid response rather than reported as "No results".
52
- - Endpoint override via `PI_DEEPSEEK_BASE_URL` or `DEEPSEEK_SEARCH_BASE_URL`, with `/v1` and trailing slashes stripped.
53
- - API key from pi's `deepseek` provider, then `DEEPSEEK_API_KEY`; redacted from all error output, and upstream error text is sliced to 400 chars.
54
- - Fetch URLs are sent normalised, and the 8192-char limit is enforced on both the raw and normalised form.
43
+ ## [0.1.2] - 2026-10-30
55
44
 
56
- **Safety**
45
+ ### Changed
57
46
 
58
- - `deepseek_fetch` refuses localhost (including trailing-dot forms), IPv4 private/loopback/CGNAT/link-local/benchmarking ranges, IPv6 `::`, `::1`, `fc00::/7`, `fe80::/10`, and IPv4-mapped IPv6 forms.
59
- - Markdown unescaping never applies inside fenced code blocks (including indented fences) or inline code spans.
60
- - Malformed result fields degrade instead of failing the whole call.
47
+ - README documents the release path: trusted publishing through GitHub OIDC, no `NPM_TOKEN`,
48
+ automatic provenance, the tag/version guard, and the option to disallow token publishing.
61
49
 
62
- **Cost and compatibility**
50
+ ## [0.1.1] - 2026-10-30
63
51
 
64
- - `estimatedCostUsd` = $0.00075 per billable request, following the raw result array so client-side filtering cannot under-report; 0 for an empty result set.
65
- - Coexists with `pi-web-access`: distinct tool names, no config keys.
66
- - Validated against DeepSeek API docs as of 2026-10-30.
52
+ ### Changed
67
53
 
68
- **Tests**
54
+ - Documentation only: install command moved up, sample output reflowed, README badges added (a
55
+ downloads badge is deferred until the package has statistics).
69
56
 
70
- - 68 `node:test` cases with mocked HTTP — no key or network required.
57
+ ## [0.1.0] - 2026-10-30
58
+
59
+ Published at $0.00075 per billable request for both Search and Fetch; failures, timeouts and empty
60
+ results are not billed. Validated against the DeepSeek API documentation as of 2026-10-30.
61
+
62
+ ### Added
63
+
64
+ - `deepseek_search` and `deepseek_fetch`, over `POST /v1/tools/search` and `POST /v1/tools/fetch`.
65
+ - Search parameters: `count` (1–50), `ori_question`, `need_content`, `localization`,
66
+ `allowed_domains`, `blocked_domains`, `lang`, `recency`. Fetch parameters: `url`, `max_chars`.
67
+ - Result metadata: publication date, authority tier, rerank score and site name, with `site_icon`,
68
+ mapped results and run metadata in `details`.
69
+ - Client-side trimming of over-length queries, reporting every removal.
70
+ - Retries for `408`, `429`, any `5xx`, network failures and timeouts, three attempts, with
71
+ `retry-after` honoured up to 5 seconds.
72
+ - `deepseek_fetch` rejects local and private targets and never unescapes inside code.
73
+
74
+
75
+ [Unreleased]: https://github.com/DumbMice/pi-deepseek-websearch/compare/v0.1.4...HEAD
76
+ [0.1.4]: https://github.com/DumbMice/pi-deepseek-websearch/compare/v0.1.3...v0.1.4
77
+ [0.1.3]: https://github.com/DumbMice/pi-deepseek-websearch/compare/v0.1.2...v0.1.3
78
+ [0.1.2]: https://github.com/DumbMice/pi-deepseek-websearch/compare/v0.1.1...v0.1.2
79
+ [0.1.1]: https://www.npmjs.com/package/@dumbmice/pi-deepseek-websearch/v/0.1.1
80
+ [0.1.0]: https://www.npmjs.com/package/@dumbmice/pi-deepseek-websearch/v/0.1.0
@@ -0,0 +1,4 @@
1
+ # Contributing
2
+
3
+ See [AGENTS.md](AGENTS.md) for the repository layout, tests, design invariants, documentation
4
+ conventions and release process.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 pi-deepseek-websearch contributors
3
+ Copyright (c) 2026 DumbMice
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,17 +1,21 @@
1
1
  # @dumbmice/pi-deepseek-websearch
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@dumbmice/pi-deepseek-websearch)](https://www.npmjs.com/package/@dumbmice/pi-deepseek-websearch)
4
- [![license](https://img.shields.io/npm/l/@dumbmice/pi-deepseek-websearch)](LICENSE)
5
- [![node](https://img.shields.io/node/v/@dumbmice/pi-deepseek-websearch)](package.json)
4
+ [![license](https://img.shields.io/npm/l/@dumbmice/pi-deepseek-websearch)](https://github.com/DumbMice/pi-deepseek-websearch/blob/main/LICENSE)
5
+ [![node](https://img.shields.io/node/v/@dumbmice/pi-deepseek-websearch)](https://github.com/DumbMice/pi-deepseek-websearch/blob/main/package.json)
6
6
  [![tests](https://github.com/DumbMice/pi-deepseek-websearch/actions/workflows/test.yml/badge.svg)](https://github.com/DumbMice/pi-deepseek-websearch/actions/workflows/test.yml)
7
- [![coexists with pi-web-access](https://img.shields.io/badge/coexists%20with-pi--web--access-2ea44f)](#why)
8
7
 
9
- DeepSeek **Search** and **Fetch** API tools for the [pi coding agent](https://pi.dev) — direct access to the fields and parameters that generic web-search wrappers throw away.
8
+ Two tools for the [pi coding agent](https://pi.dev), wrapping DeepSeek's Search and Fetch APIs.
9
+ `deepseek_search` returns ranked web results with a publication date, authority tier and rerank score
10
+ per hit, plus `ori_question` and `localization` for steering the ranking. `deepseek_fetch` returns a
11
+ page as Markdown.
10
12
 
11
- Two tools, no dependencies, and it **coexists with `pi-web-access`**: different tool names, no web-search.json keys of its own, no settings to migrate.
13
+ ## Example output
14
+
15
+ `deepseek_search` with `need_content: true` and `count: 2`, second result omitted:
12
16
 
13
17
  ```text
14
- DeepSeek search · 2 results ← abridged
18
+ DeepSeek search · 2 results
15
19
  query: DeepSeek V4 Pro release
16
20
 
17
21
  1. DeepSeek-V4-Pro GA Release | DeepSeek API Docs
@@ -21,23 +25,14 @@ query: DeepSeek V4 Pro release
21
25
  We're launching DeepSeek-V4-Pro today! …
22
26
  ```
23
27
 
24
- Every result also carries site, publication date, authority tier and rerank score; `details` holds the full set.
25
-
26
- ## Why
27
-
28
- `pi-web-access` maps DeepSeek's Search API onto its generic provider interface (compared against `pi-web-access@0.44.0`, verified 2026-10-30). That is the right shape for a 40-provider router, but it loses what makes DeepSeek's API useful:
28
+ ## When to use it
29
29
 
30
- | | `pi-web-access` DeepSeek provider | this extension |
31
- |---|---|---|
32
- | `date`, `authority`, `rerank_score` | dropped | rendered per result |
33
- | `ori_question` (ranking hint) | never sent | `ori_question` param |
34
- | `localization` | never sent | `localization` param |
35
- | `count` | hidden, clamped to 20 | `count` param, 1–50 |
36
- | query > 128 chars | hard error, no fallback | trimmed, with the removals reported |
37
- | 429 / 5xx | no retry | 3 attempts honouring `retry-after` |
38
- | `need_content` | only via a generic flag | `need_content` param |
39
-
40
- Use it when you need DeepSeek's ranking, publication dates, or a specific `count`. Keep `pi-web-access` for routing across providers, `fetch_content`'s extraction chain, and `source_check`.
30
+ - The answer depends on **when** something was published, or on an authoritative source — the results
31
+ carry `date` and `authority` where a generic search returns bare snippets.
32
+ - The agent needs a **specific count**, from 1 to 50.
33
+ - The user's original question can ride along as `ori_question`, which improves ranking.
34
+ - The agent should read the page rather than the snippet: `need_content` returns extracted text, and
35
+ `deepseek_fetch` reads a known URL.
41
36
 
42
37
  ## Install
43
38
 
@@ -45,32 +40,21 @@ Use it when you need DeepSeek's ranking, publication dates, or a specific `count
45
40
  # from npm
46
41
  pi install npm:@dumbmice/pi-deepseek-websearch
47
42
 
48
- # straight from GitHub
43
+ # straight from the repository
49
44
  pi install git:github.com/DumbMice/pi-deepseek-websearch
50
-
51
- # from a working copy (no publish required)
52
- pi install /absolute/path/to/pi-deepseek-websearch
53
-
54
- # try it for one session
55
- pi -e /absolute/path/to/pi-deepseek-websearch
56
45
  ```
57
46
 
58
- Requires Node >= 22.6 (`engines`; the shipped entry point is TypeScript).
59
-
60
- If you previously installed a single-file copy of this extension in `~/.pi/agent/extensions/`, remove it first — two copies would register `deepseek_search` twice. `pi-web-access` itself needs no change.
61
-
62
- Confirm it registered: start `pi`, ask *"use deepseek_search to look up the DeepSeek API pricing"*, and you should see a `DeepSeek search · N results` header. Or check the tool list in your session.
63
-
64
- ## Credentials
65
-
66
- The extension uses, in order:
67
-
68
- 1. pi's `deepseek` provider credentials (`ctx.modelRegistry.getApiKeyForProvider("deepseek")`) — i.e. `/login` or `auth.json`;
69
- 2. `DEEPSEEK_API_KEY`.
47
+ Requirements:
70
48
 
71
- Endpoint override: `PI_DEEPSEEK_BASE_URL`, then `DEEPSEEK_SEARCH_BASE_URL` (default `https://api.deepseek.com`; a trailing slash or `/v1` suffix is stripped). The bare `DEEPSEEK_BASE_URL` is deliberately ignored: other tools commonly set it to an OpenAI-compatible `/v1` root.
49
+ - Node >= 22.6.
50
+ - A DeepSeek key, from pi's `deepseek` provider (`/login`) or the `DEEPSEEK_API_KEY` variable. Create
51
+ one at <https://platform.deepseek.com/api_keys>.
52
+ - Optional, for a compatible gateway: set `PI_DEEPSEEK_BASE_URL` or `DEEPSEEK_SEARCH_BASE_URL`. A
53
+ trailing slash or `/v1` suffix is stripped. The bare `DEEPSEEK_BASE_URL` is ignored on purpose,
54
+ because other tools point it at an OpenAI-compatible `/v1` root.
72
55
 
73
- Get a key at <https://platform.deepseek.com/api_keys>.
56
+ Confirm registration by starting `pi` and asking *"use deepseek_search to look up the DeepSeek API
57
+ pricing"*; the reply begins with a `DeepSeek search · N result(s)` header.
74
58
 
75
59
  ## Tools
76
60
 
@@ -78,114 +62,80 @@ Get a key at <https://platform.deepseek.com/api_keys>.
78
62
 
79
63
  | Parameter | Type | Notes |
80
64
  |---|---|---|
81
- | `query` | string, required | Operators pass through: `"exact phrase"`, `-word`, `-"phrase"`, `site:a OR site:b`, `-site:c`, `lang:en,zh`, `after:2025-01-01`, `before:2026-06-01`. Capped at 128 chars; longer queries are trimmed (see below) rather than rejected |
82
- | `count` | integer 1–50 | default 10; the per-request price does not depend on it |
83
- | `ori_question` | string (cut to 128) | your original question, used for ranking; a longer value is cut to 128 code points and the cut is reported in the output |
84
- | `need_content` | boolean | return extracted page text when the API provides it (otherwise the `summary`, or the `snippet` if there is no summary), capped at 4,000 chars per result with truncation reported; same price, more context |
85
- | `allowed_domains` | string[] | compiled to `site:` clauses joined with `OR`; counts toward the 128-char cap |
65
+ | `query` | string, required | capped at 128 characters, operators included |
66
+ | `count` | integer 1–50 | default 10 |
67
+ | `ori_question` | string | the user's question, used for ranking; values over 128 characters are cut and the cut is reported |
68
+ | `need_content` | boolean | return extracted page text when available, otherwise the summary or snippet; 4,000 characters per result, truncation reported |
69
+ | `allowed_domains` | string[] | compiled to `site:` clauses; counts toward the 128-character cap |
86
70
  | `blocked_domains` | string[] | compiled to `-site:` clauses; counts toward the cap |
87
- | `lang` | string | e.g. `"zh,en"` → `lang:zh,en` |
88
- | `recency` | `day`\|`week`\|`month`\|`year` | compiled to `after:<date>` |
89
- | `localization` | string | IPv4/IPv6 literal used to localize results; anything else is rejected |
71
+ | `lang` | string | e.g. `"zh,en"` becomes `lang:zh,en` |
72
+ | `recency` | `day` \| `week` \| `month` \| `year` | compiled to `after:<date>` |
73
+ | `localization` | string | optional IPv4 or IPv6 literal; results are localized to that address's region; country codes are rejected |
90
74
 
91
- Each result carries title, URL, site, publication date, authority tier (`0–4`; the API's `-1` renders as *authority unknown*), and rerank score (`≥1.5` annotated as high quality), plus the model-oriented `summary` or the extracted page text. `site_icon` is not rendered but is included in `details`.
75
+ `query` accepts DeepSeek's operators unchanged: `"exact phrase"`, `-word`, `-"phrase"`,
76
+ `site:a.example OR site:b.example`, `-site:c.example`, `lang:en,zh`, `after:2025-01-01`,
77
+ `before:2026-06-01`.
92
78
 
93
- `details` also reports: the final `query`, `dropped` (anything trimmed), `count` (results returned), `requestedCount`, `discarded` (unusable results, e.g. no `http(s)` URL), `oriQuestionTruncated`, `needContent`, the mapped `results[]`, and `estimatedCostUsd`.
79
+ Each result shows title, URL, site, date, authority tier (`0–4`; the API's `-1` appears as *authority
80
+ unknown*), rerank score (`≥1.5` marked high quality), and the summary or extracted text. Results
81
+ without an `http(s)` URL are discarded, and the reply reports how many.
82
+
83
+ `details` carries the same call in structured form: the final query, the requested and returned
84
+ counts, anything trimmed or discarded, one record per result, and the estimated cost.
94
85
 
95
86
  ### `deepseek_fetch`
96
87
 
97
88
  | Parameter | Type | Notes |
98
89
  |---|---|---|
99
- | `url` | string, required | absolute public HTTP(S), ≤8192 chars, no credentials; local/private targets are refused |
100
- | `max_chars` | integer 500–200000 | default 20000; truncation and clamping are both reported |
101
-
102
- Returns the page as Markdown, rendered and extracted server-side. A page with no extractable content is an error rather than an empty success. `details` reports `url`, `title`, `chars`, `maxChars`, `truncated` and `estimatedCostUsd`.
103
-
104
- ## Behaviour worth knowing
105
-
106
- - **The 128-character limit is real** and includes `site:`, `lang:` and `after:` operators. When the query fits, it is sent exactly as written, spacing included. Only an over-long query is rewritten, and then filters are shed first (recency → language → domains), then trailing words, then — only if a single token exceeds the whole budget — the tail is cut. Every trimming removal is printed as `trimmed to fit 128 chars: …`, so the query itself is never silently shortened. Trimming is token-aware: quoted phrases stay whole, and a trailing operator (`site:`, `after:`, a bare `-`) is dropped rather than left dangling.
107
- - **Retries** cover `408`, `429` and **any** `5xx` — the documented "wait and retry" set — plus network failures and timeouts, up to 3 attempts. `retry-after` is honoured in seconds or HTTP-date form, capped at 5 s. Every other `4xx` fails immediately.
108
- - **Hints** accompany `401` (key), `402` (top up), `403`/`404` on Fetch (security policy, no usable content), `413` (body too large) and `429` (rate limits); `400` surfaces the API's own message.
109
- - **Billing**: $0.75 per 1,000 successful requests for both Search and Fetch (<https://api-docs.deepseek.com/guides/search#billing>). A request is billable when it returns HTTP 200 **and** a non-empty result, so `estimatedCostUsd` follows the raw result array even if every URL is later discarded. Failed calls, empty search results and timeouts are reported as free, because no HTTP 200 is observed for them. `count` does not affect price.
110
- - The docs' note that server-side tool calls are free until 2026-11-01 00:00 Beijing time is scoped to DeepSeek Harness / Claude Code / Codex usage; this extension calls the API directly, so the published per-request price applies.
111
- - **Limits**: 100 QPS search, 25 QPS fetch per account, plus a per-domain fetch limit (<https://api-docs.deepseek.com/guides/search#rate-limits>).
112
- - **Empty is not an error**: a search with no matches returns a normal "No results" answer and costs nothing.
113
-
114
- ## Safety
115
-
116
- - The API key is redacted from every error message before it reaches the model or the transcript.
117
- - `deepseek_fetch` refuses `localhost` and `*.localhost` (including trailing-dot forms), IPv4 loopback/private/CGNAT/link-local/benchmarking ranges, `::`, `::1`, `fc00::/7`, `fe80::/10`, and IPv4-mapped IPv6 forms such as `::ffff:127.0.0.1`.
118
- - This is a **policy** choice, not a docs requirement: DeepSeek fetches pages server-side, so the guard exists to avoid handing intranet URLs to a third party. It matches literal addresses and localhost names only — a public hostname that resolves to a private address is not caught, because no DNS lookup is performed.
119
- - Markdown unescaping of Fetch output (DeepSeek escapes punctuation that was already plain text) never touches fenced blocks — including fences indented inside list items — or inline code spans.
120
-
121
- ## Docs alignment
122
-
123
- Validated against <https://api-docs.deepseek.com/guides/search>, `/api/search` and `/api/fetch` on **2026-10-30** (`DOCS_SNAPSHOT` in `src/index.ts`). The wire contract is followed exactly where the docs define it: endpoints, auth headers, request fields and defaults, response field semantics, price, QPS and the documented error taxonomy.
124
-
125
- Client-side deviations and transport details, listed in full. Items producing no signal in the output are marked _(silent)_.
126
-
127
- 1. over-length queries are trimmed rather than rejected, and every removal is reported; a single token longer than the whole budget is cut mid-token;
128
- 2. query whitespace is normalised only when the query must be rewritten, and that is reported in the same line;
129
- 3. leading/trailing whitespace is stripped from `query`, `ori_question` and `localization` before use _(silent — the API rejects all-whitespace queries anyway)_;
130
- 4. result body text (extracted content, else `summary`, else `snippet`) is capped at 4,000 chars per result; the true length is printed;
131
- 5. `count` is clamped to 1–50 instead of the API returning 400 — the schema declares the range too, and clamping is the fallback if a host passes values through unvalidated; `details.requestedCount` still shows the original ask;
132
- 6. results without an `http(s)` URL are discarded defensively — the count is reported and the price still reflects the raw response;
133
- 7. `site_icon` is surfaced in `details` but not rendered;
134
- 8. `ori_question` longer than 128 code points is cut, and the cut is reported in the output and `details`;
135
- 9. `deepseek_fetch` blocks local/private addresses (docs impose no such rule);
136
- 10. Fetch Markdown is unescaped heuristically (docs only promise Markdown) _(silent; never inside code)_;
137
- 11. `max_chars` truncates Fetch output and clamps out-of-range values (again, the schema declares the range), both reported;
138
- 12. a 200 response with empty Fetch content is treated as an error (docs route empty extraction to 404);
139
- 13. `date` is displayed as `YYYY-MM-DD` while the API returns RFC 3339 — the full timestamp stays in `details`;
140
- 14. the Fetch `url` is sent in its normalised form (`URL.toString()`: IDN punycoding, lowercased host, added `/`), and the 8192-char limit is checked against both the raw and the normalised form;
141
- 15. redirects of the API request itself are not followed (`redirect: "error"` on the call to `api.deepseek.com`), so a 3xx from the API surfaces as a fetch error and is retried like a network failure (item 16). Target-page redirects are DeepSeek's concern, not this client's _(silent)_;
142
- 16. network failures and timeouts are retried beyond the documented `408/429/5xx` list (they are unbilled) _(silent)_;
143
- 17. `retry-after` support is an inference — the docs never mention the header — and the delay is capped at 5 s _(silent)_;
144
- 18. the 128- and 8192-character limits are counted in code points. The docs say "1 to 128 Unicode characters" for `query` and "characters" elsewhere, without fixing code points vs UTF-16 units _(silent)_;
145
- 19. client request timeouts are 30 s (search) and 60 s (fetch) **per attempt**, so a call that keeps timing out can run ~90 s (search) or ~180 s (fetch) plus backoff before the third attempt fails; the docs describe no response deadline _(silent)_;
146
- 20. a 200 search body without a `results` array is rejected as an invalid response instead of being reported as "No results";
147
- 21. `localization` must be an IP literal — the docs only say "an optional IP address", so a country code is rejected client-side;
148
- 22. upstream error messages are sliced to 400 characters before display _(silent)_;
149
- 23. result fields of an unexpected type are ignored rather than passed through, so one malformed result degrades instead of failing the call _(silent)_;
150
- 24. `recency` windows are fixed client-side at 1 / 7 / 30 / 365 days for `day` / `week` / `month` / `year`.
151
-
152
- Transport details, for completeness: requests send `Accept: application/json` alongside the two documented headers; retry backoff is 500 ms then 1 s when no `retry-after` is present (two sleeps for three attempts); and a 200 response with a non-JSON body fails immediately without a retry.
153
-
154
- If DeepSeek changes the API, bump `DOCS_SNAPSHOT` and update the affected tests.
155
-
156
- ## Development
157
-
158
- Requires Node >= 22.6 (the test runner uses `--experimental-strip-types`).
159
-
160
- ```bash
161
- npm test # node:test, 68 tests, mocked HTTP — no API key or network needed
162
- npm run typecheck # tsc --noEmit, strict
163
- npm run pack:check # tarball contents (no tests, no devDependencies)
164
- ```
165
-
166
- The suite covers query trimming order and the 128 cap, caller-spacing preservation, documented validation rules, metadata rendering (`authority: -1`, score annotation, summary/snippet/content precedence, malformed fields), retry classification for every documented status, key redaction, URL safety including encoding bypasses, abort handling, and both tool call shapes.
167
-
168
- ## Releasing
169
-
170
- Versions are published from CI with npm [trusted publishing](https://docs.npmjs.com/trusted-publishers):
171
- the workflow exchanges a GitHub OIDC token for a short-lived publish credential, so the repository
172
- holds **no `NPM_TOKEN` secret** and no long-lived write credential (including a 2FA-bypass token)
173
- is needed or permitted. Because the repository is public, npm attaches a **provenance attestation**
174
- automatically — no `--provenance` flag.
175
-
176
- ```bash
177
- npm version patch --no-git-tag-version # or minor/major
178
- git commit -am "chore: release v$(node -p "require('./package.json').version")"
179
- git tag "v$(node -p "require('./package.json').version")"
180
- git push origin main --follow-tags # .github/workflows/publish.yml runs test + typecheck, then publishes
181
- ```
182
-
183
- `.github/workflows/publish.yml` runs the 68 tests and the typecheck, and refuses to publish when the
184
- pushed tag and `package.json` version disagree, so a mis-versioned tag fails the build instead of
185
- shipping the wrong number. For maximum safety the package's *Publishing access* can then be set to
186
- "Require two-factor authentication and disallow tokens": trusted publishing keeps working, token
187
- publishing stops.
90
+ | `url` | string, required | absolute public HTTP(S), at most 8192 characters, no credentials |
91
+ | `max_chars` | integer 500–200000 | default 20000; truncation and clamping are reported |
92
+
93
+ Returns the page as Markdown, extracted server-side. A page with no extractable content is reported as
94
+ an error, not as an empty success.
95
+
96
+ ## Cost and limits
97
+
98
+ - $0.75 per 1,000 successful requests for both tools
99
+ ([billing](https://api-docs.deepseek.com/guides/search#billing)). A search is billable only when the
100
+ API returns HTTP 200 with a non-empty `results` array, and a fetch only when it returns page
101
+ content, so failures, timeouts and empty searches cost nothing. `count` does not change the price.
102
+ - 100 QPS for Search, 25 QPS for Fetch per account, plus a per-domain Fetch limit
103
+ ([rate limits](https://api-docs.deepseek.com/guides/search#rate-limits)).
104
+
105
+ ## Behaviour to expect
106
+
107
+ - **Long queries are trimmed, not rejected.** The 128-character cap includes operators; when a query
108
+ is shortened, the reply names what went — filters first, then trailing words, then a truncated tail
109
+ — and prints the final query.
110
+ - **Transient failures are retried.** `408`, `429`, any `5xx`, network failures and timeouts get up
111
+ to three attempts, honouring `retry-after` (seconds or an HTTP date) capped at 5 seconds. Every
112
+ other `4xx` fails immediately with DeepSeek's own message. Each attempt has its own deadline: 30
113
+ seconds for search, 60 for fetch.
114
+ - **A search with no matches is a normal answer**, not an error.
115
+ - **Private targets are refused.** `deepseek_fetch` will not fetch `localhost` or private address
116
+ ranges. This is deliberate: DeepSeek fetches pages server-side, and the guard avoids handing it
117
+ intranet URLs. It matches literal addresses only — no DNS lookup is performed.
118
+ - **The API key is redacted** from upstream error text and network-failure messages before they reach
119
+ the model or the transcript.
120
+
121
+ ## Troubleshooting
122
+
123
+ - **`deepseek_search` appears twice in the tool list.** Remove the older copy — for example a copy you
124
+ placed directly in `~/.pi/agent/extensions/` — then `/reload`.
125
+ - **`DeepSeek API key not found`.** Create a key at <https://platform.deepseek.com/api_keys>,
126
+ configure pi's `deepseek` provider with `/login`, or set `DEEPSEEK_API_KEY`.
127
+ - **The output says `trimmed to fit 128 chars`.** The request was shortened; shorten the query or drop
128
+ filters to avoid it.
129
+
130
+ ## Further reading
131
+
132
+ - **Deviations from DeepSeek's API** — every deliberate difference:
133
+ [docs/docs-alignment.md](https://github.com/DumbMice/pi-deepseek-websearch/blob/main/docs/docs-alignment.md)
134
+ - **Working on this repository** — layout, tests, invariants, release process:
135
+ [AGENTS.md](https://github.com/DumbMice/pi-deepseek-websearch/blob/main/AGENTS.md)
136
+ - **Changes by version**:
137
+ [CHANGELOG.md](https://github.com/DumbMice/pi-deepseek-websearch/blob/main/CHANGELOG.md)
188
138
 
189
139
  ## License
190
140
 
191
- MIT. Issues and source: <https://github.com/DumbMice/pi-deepseek-websearch>.
141
+ MIT. Source and issues: <https://github.com/DumbMice/pi-deepseek-websearch>.
@@ -0,0 +1,81 @@
1
+ # DeepSeek API alignment
2
+
3
+ How this extension's behaviour relates to DeepSeek's published Search & Fetch API contract,
4
+ validated against <https://api-docs.deepseek.com/guides/search>, `/api/search` and `/api/fetch`
5
+ on **2026-10-30** (`DOCS_SNAPSHOT` in `src/index.ts`).
6
+
7
+ The wire contract is followed where the docs define it: endpoints, auth headers, request fields and
8
+ defaults, response field semantics, price, QPS, and the documented error taxonomy. A request this
9
+ extension can emit is always a valid request.
10
+
11
+ Client-side deviations and transport details, listed in full. Items that produce no signal in the
12
+ output are marked _(silent)_; everything else is observable in the reply or in `details`.
13
+
14
+ 1. over-length queries are trimmed rather than rejected, and every removal is reported; a single
15
+ token longer than the whole budget is cut mid-token;
16
+ 2. query whitespace is normalised only when the query must be rewritten, and that is reported in the
17
+ same line;
18
+ 3. leading and trailing whitespace is stripped from `query`, `ori_question` and `localization` before
19
+ use _(silent — for `query` the API rejects an all-whitespace value; the other two are trimmed for
20
+ hygiene)_;
21
+ 4. result body text is the extracted `content` when `need_content` is set, otherwise the `summary`,
22
+ else the `snippet`; the rendered text is capped at 4,000 chars per result and the true length is
23
+ printed;
24
+ 5. `count` is clamped to 1–50 instead of the API returning 400 — the schema declares the range too,
25
+ and clamping is the fallback if a host passes values through unvalidated; `details.requestedCount`
26
+ still shows the caller's numeric ask (floored; a non-numeric or non-finite ask is reported as 10);
27
+ 6. results without an `http(s)` URL are discarded defensively — the count is reported and the price
28
+ still reflects the raw response;
29
+ 7. `site_icon` is surfaced as `siteIcon` in `details` but not rendered;
30
+ 8. `ori_question` longer than 128 code points is cut, and the cut is reported in the output and
31
+ `details`;
32
+ 9. `deepseek_fetch` blocks local and private addresses (the docs impose no such rule). IPv6 forms
33
+ beyond `::ffff:`, `::/96` and NAT64 `64:ff9b::/96` — 6to4 and RFC 8215 — are out of scope;
34
+ 10. Fetch Markdown is unescaped heuristically (the docs only promise Markdown) _(silent; never inside
35
+ code)_;
36
+ 11. `max_chars` truncates Fetch output and clamps out-of-range values (again, the schema declares the
37
+ range), both reported;
38
+ 12. a 200 response with empty Fetch content is treated as an error (the docs route empty extraction to
39
+ 404);
40
+ 13. `date` is displayed as `YYYY-MM-DD` while the API returns RFC 3339 — the full timestamp stays in
41
+ `details`;
42
+ 14. the Fetch `url` is sent in its normalised form (`URL.toString()`: IDN punycoding, lowercased host,
43
+ added `/`), and the 8192-character limit is checked against both the raw and the normalised form;
44
+ 15. redirects of the API request itself are not followed (`redirect: "error"` on the call to
45
+ `api.deepseek.com`), so a 3xx from the API surfaces as a fetch error and is retried like a network
46
+ failure (item 16); target-page redirects are DeepSeek's concern, not this client's. The failure
47
+ message names the redirect rather than the redirect target _(silent)_;
48
+ 16. network failures and timeouts are retried beyond the documented `408`/`429`/`5xx` list (they are
49
+ unbilled) _(silent)_;
50
+ 17. `retry-after` support is an inference — the docs never mention the header — and the delay is
51
+ capped at 5 s _(silent)_;
52
+ 18. the 128- and 8192-character limits are counted in code points. The docs say "1 to 128 Unicode
53
+ characters" for `query` and "characters" elsewhere, without fixing code points versus UTF-16
54
+ units _(silent)_;
55
+ 19. client request timeouts are 30 s (search) and 60 s (fetch) **per attempt**, so a call that keeps
56
+ timing out can run ~90 s (search) or ~180 s (fetch) plus backoff before the third attempt fails;
57
+ the docs describe no response deadline. The duration appears in the failure message;
58
+ 20. a 200 search body without a `results` array is rejected as an invalid response instead of being
59
+ reported as "No results";
60
+ 21. `localization` must be an IP literal — the docs only say "an optional IP address", so a country
61
+ code is rejected client-side;
62
+ 22. upstream error messages are sliced to 400 characters before display _(silent)_;
63
+ 23. result fields of an unexpected type are ignored rather than passed through, so one malformed
64
+ result degrades instead of failing the call _(silent)_;
65
+ 24. `recency` windows are fixed client-side at 1 / 7 / 30 / 365 days for `day` / `week` / `month` /
66
+ `year`.
67
+
68
+ Transport details: requests send `Accept: application/json` alongside the two documented headers;
69
+ retry backoff is 500 ms then 1 s when no `retry-after` is present (two sleeps for three attempts); and
70
+ a 200 response with a non-JSON body fails immediately without a retry.
71
+
72
+ ## Maintaining this list
73
+
74
+ Most items are pinned by a test in `test/index.test.ts`. The exceptions are transport-only details
75
+ asserted by inspection — the per-attempt timeout values (item 19) and the `recency` day counts
76
+ (item 24, pinned only as an `after:<date>` clause).
77
+
78
+ Changing a documented behaviour means changing its item here, the matching README text and the test in
79
+ the same commit. `DOCS_SNAPSHOT` lives in `src/index.ts` (header comment and constant) and at the
80
+ top of this file — keep them in step. When DeepSeek revises the API, bump
81
+ that date and re-audit every item.
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@dumbmice/pi-deepseek-websearch",
3
- "version": "0.1.2",
4
- "description": "Web search and page fetch for pi via DeepSeek's Search & Fetch APIs: publication dates, authority, rerank score, ori_question, count up to 50, and graceful 128-char handling. Coexists with pi-web-access.",
3
+ "version": "0.1.4",
4
+ "description": "DeepSeek web search and page fetch tools for the pi coding agent: publication dates, authority, rerank scores, ori_question, count 1-50.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": {
8
- "name": "yangbangcheng",
8
+ "name": "DumbMice",
9
9
  "email": "bangchengyang@hotmail.com"
10
10
  },
11
11
  "homepage": "https://github.com/DumbMice/pi-deepseek-websearch#readme",
@@ -18,18 +18,20 @@
18
18
  "pi-package",
19
19
  "pi-extension",
20
20
  "pi-coding-agent",
21
- "pi",
22
21
  "deepseek",
22
+ "deepseek-api",
23
23
  "web-search",
24
24
  "web-fetch",
25
25
  "search-api",
26
26
  "llm-tools",
27
- "websearch",
28
- "deepseek-search"
27
+ "ai-coding-agent"
29
28
  ],
30
29
  "files": [
31
30
  "src",
31
+ "docs",
32
32
  "README.md",
33
+ "AGENTS.md",
34
+ "CONTRIBUTING.md",
33
35
  "LICENSE",
34
36
  "CHANGELOG.md"
35
37
  ],
package/src/index.ts CHANGED
@@ -30,7 +30,7 @@ const FETCH_TIMEOUT_MS = 60_000;
30
30
  const MAX_ATTEMPTS = 3;
31
31
  const MAX_ORI_QUESTION_CHARS = 128;
32
32
  const MAX_RESULT_CONTENT_CHARS = 4_000;
33
- const MAX_FETCH_CONTENT_CHARS = 20_000;
33
+ const DEFAULT_FETCH_MAX_CHARS = 20_000;
34
34
 
35
35
  export interface SearchOptions {
36
36
  query: string;
@@ -134,7 +134,7 @@ function isRetryableStatus(status: number): boolean {
134
134
  return status === 408 || status === 429 || status >= 500;
135
135
  }
136
136
 
137
- function retryDelayMs(response: Response, attempt: number): number {
137
+ export function retryDelayMs(response: Response, attempt: number): number {
138
138
  const RETRY_CAP_MS = 5_000;
139
139
  const header = response.headers.get("retry-after");
140
140
  if (header) {
@@ -208,17 +208,23 @@ async function postDeepSeek<T>(
208
208
  try {
209
209
  return (await response.json()) as T;
210
210
  } catch {
211
+ if (signal?.aborted) throw abortError(signal);
211
212
  throw new Error(`DeepSeek ${path} returned invalid JSON`);
212
213
  }
213
214
  }
214
215
 
215
- const raw = await response.text().catch(() => "");
216
+ let raw = "";
217
+ try {
218
+ raw = await response.text();
219
+ } catch {
220
+ if (signal?.aborted) throw abortError(signal);
221
+ }
216
222
  let message = raw;
217
223
  try {
218
224
  const parsed = JSON.parse(raw) as { error?: { message?: string } };
219
225
  message = parsed.error?.message?.trim() || raw;
220
226
  } catch {}
221
- message = REDACT(message.slice(0, 400), apiKey);
227
+ message = REDACT(message, apiKey).slice(0, 400);
222
228
 
223
229
  const error = new DeepSeekHttpError(
224
230
  response.status,
@@ -553,7 +559,7 @@ const FETCH_SCHEMA = {
553
559
  type: "integer",
554
560
  minimum: 500,
555
561
  maximum: 200_000,
556
- description: `Return at most this many characters (default ${MAX_FETCH_CONTENT_CHARS}). Values outside 500-200000 are clamped, and the clamp is reported in the output.`,
562
+ description: `Return at most this many characters (default ${DEFAULT_FETCH_MAX_CHARS}). Values outside 500-200000 are clamped, and the clamp is reported in the output.`,
557
563
  },
558
564
  },
559
565
  required: ["url"],
@@ -587,7 +593,7 @@ export default function (pi: any): void {
587
593
  return value
588
594
  .filter((entry): entry is string => typeof entry === "string")
589
595
  .map((entry) => entry.trim())
590
- .filter(Boolean);
596
+ .filter((entry) => entry.length > 0 && !/[\s"]/.test(entry));
591
597
  };
592
598
  const oriChars = Array.from(
593
599
  typeof params.ori_question === "string" ? params.ori_question.trim() : "",
@@ -706,8 +712,8 @@ export default function (pi: any): void {
706
712
  ctx: any,
707
713
  ) {
708
714
  const url = assertFetchableUrl(String(params.url ?? ""));
709
- const requestedChars = Number(params.max_chars ?? MAX_FETCH_CONTENT_CHARS);
710
- const requested = Number.isFinite(requestedChars) ? Math.floor(requestedChars) : MAX_FETCH_CONTENT_CHARS;
715
+ const requestedChars = Number(params.max_chars ?? DEFAULT_FETCH_MAX_CHARS);
716
+ const requested = Number.isFinite(requestedChars) ? Math.floor(requestedChars) : DEFAULT_FETCH_MAX_CHARS;
711
717
  const maxChars = Math.min(200_000, Math.max(500, requested));
712
718
  const clamped = maxChars !== requested;
713
719
  const apiKey = await resolveApiKey(ctx);
@@ -728,15 +734,14 @@ export default function (pi: any): void {
728
734
 
729
735
  const total = charLength(content);
730
736
  const body = total > maxChars ? Array.from(content).slice(0, maxChars).join("") : content;
731
- const text = [
737
+ const head = [
732
738
  `DeepSeek fetch · ${total} chars${total > maxChars ? ` (truncated to ${maxChars})` : ""}${clamped ? ` (max_chars clamped to ${maxChars})` : ""}`,
733
739
  title ? `title: ${title}` : "",
734
740
  url.toString(),
735
- "",
736
- body,
737
741
  ]
738
- .filter((line) => line !== "")
742
+ .filter(Boolean)
739
743
  .join("\n");
744
+ const text = `${head}\n\n${body}`;
740
745
 
741
746
  return {
742
747
  content: [{ type: "text", text }],