@dumbmice/pi-deepseek-websearch 0.1.3 → 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 +156 -0
- package/CHANGELOG.md +58 -53
- package/CONTRIBUTING.md +4 -0
- package/LICENSE +1 -1
- package/README.md +93 -114
- package/docs/docs-alignment.md +81 -0
- package/package.json +8 -6
- package/src/index.ts +17 -12
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,75 +1,80 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
CONTRIBUTING.md, with a single pointer left behind.
|
|
7
|
+
## [Unreleased]
|
|
7
8
|
|
|
8
|
-
## 0.1.
|
|
9
|
+
## [0.1.4] - 2026-10-30
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
`NPM_TOKEN`, automatic provenance, the tag/version guard, and the option to disallow
|
|
12
|
-
token publishing once trusted publishing is proven.
|
|
11
|
+
### Added
|
|
13
12
|
|
|
14
|
-
|
|
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.
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
republishes the corrected text.
|
|
18
|
+
### Changed
|
|
18
19
|
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
### Fixed
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
## [0.1.3] - 2026-10-30
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
- Blank/whitespace-only `query` is rejected locally with a clear message.
|
|
37
|
-
- Search parameters: `count` 1–50, `ori_question`, `need_content`, `localization`, `allowed_domains`, `blocked_domains`, `lang`, `recency`.
|
|
38
|
-
- Fetch parameters: `url`, `max_chars` (500–200,000, default 20,000).
|
|
39
|
-
- Result metadata: `date`, `authority` (`-1` → *unknown*), `rerank_score` (≥1.5 annotated), `site_name`; `site_icon` available in `details`.
|
|
40
|
-
- `details` reports the final query, anything trimmed, discarded non-http(s) results, and `estimatedCostUsd`.
|
|
38
|
+
### Changed
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
- README kept user-facing: development and release instructions moved to `CONTRIBUTING.md`, with a
|
|
41
|
+
single pointer left behind.
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
- 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.
|
|
46
|
-
- Trimming is token-aware: quoted phrases stay whole and a trailing operator is never left dangling.
|
|
47
|
-
- `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.
|
|
48
|
-
- `details.requestedCount` reports the pre-clamp `count` ask.
|
|
49
|
-
- Unknown `recency` values and non-IP `localization` values are rejected with an actionable message.
|
|
43
|
+
## [0.1.2] - 2026-10-30
|
|
50
44
|
|
|
51
|
-
|
|
45
|
+
### Changed
|
|
52
46
|
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
- 30 s search / 60 s fetch timeouts; the caller's abort reason is preserved on every path, including during retry backoff.
|
|
56
|
-
- A 200 search response without a `results` array is rejected as an invalid response rather than reported as "No results".
|
|
57
|
-
- Endpoint override via `PI_DEEPSEEK_BASE_URL` or `DEEPSEEK_SEARCH_BASE_URL`, with `/v1` and trailing slashes stripped.
|
|
58
|
-
- 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.
|
|
59
|
-
- Fetch URLs are sent normalised, and the 8192-char limit is enforced on both the raw and normalised form.
|
|
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.
|
|
60
49
|
|
|
61
|
-
|
|
50
|
+
## [0.1.1] - 2026-10-30
|
|
62
51
|
|
|
63
|
-
|
|
64
|
-
- Markdown unescaping never applies inside fenced code blocks (including indented fences) or inline code spans.
|
|
65
|
-
- Malformed result fields degrade instead of failing the whole call.
|
|
52
|
+
### Changed
|
|
66
53
|
|
|
67
|
-
|
|
54
|
+
- Documentation only: install command moved up, sample output reflowed, README badges added (a
|
|
55
|
+
downloads badge is deferred until the package has statistics).
|
|
68
56
|
|
|
69
|
-
|
|
70
|
-
- Coexists with `pi-web-access`: distinct tool names, no config keys.
|
|
71
|
-
- Validated against DeepSeek API docs as of 2026-10-30.
|
|
57
|
+
## [0.1.0] - 2026-10-30
|
|
72
58
|
|
|
73
|
-
|
|
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.
|
|
74
61
|
|
|
75
|
-
|
|
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
|
package/CONTRIBUTING.md
ADDED
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
# @dumbmice/pi-deepseek-websearch
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@dumbmice/pi-deepseek-websearch)
|
|
4
|
-
[](LICENSE)
|
|
5
|
-
[](package.json)
|
|
4
|
+
[](https://github.com/DumbMice/pi-deepseek-websearch/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/DumbMice/pi-deepseek-websearch/blob/main/package.json)
|
|
6
6
|
[](https://github.com/DumbMice/pi-deepseek-websearch/actions/workflows/test.yml)
|
|
7
|
-
[](#why)
|
|
8
7
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
## Why
|
|
28
|
+
## When to use it
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
47
|
+
Requirements:
|
|
59
48
|
|
|
60
|
-
|
|
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.
|
|
61
55
|
|
|
62
|
-
Confirm
|
|
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`.
|
|
70
|
-
|
|
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.
|
|
72
|
-
|
|
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,85 +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 |
|
|
82
|
-
| `count` | integer 1–50 | default 10
|
|
83
|
-
| `ori_question` | string
|
|
84
|
-
| `need_content` | boolean | return extracted page text when
|
|
85
|
-
| `allowed_domains` | string[] | compiled to `site:` clauses
|
|
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"`
|
|
88
|
-
| `recency` | `day
|
|
89
|
-
| `localization` | string | IPv4
|
|
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 |
|
|
74
|
+
|
|
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`.
|
|
90
78
|
|
|
91
|
-
Each result
|
|
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.
|
|
92
82
|
|
|
93
|
-
`details`
|
|
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),
|
|
100
|
-
| `max_chars` | integer 500–200000 | default 20000; truncation and clamping are
|
|
101
|
-
|
|
102
|
-
Returns the page as Markdown,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
|
|
114
|
-
##
|
|
115
|
-
|
|
116
|
-
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
## Contributing
|
|
157
|
-
|
|
158
|
-
Development setup, tests and the release process live in [CONTRIBUTING.md](https://github.com/DumbMice/pi-deepseek-websearch/blob/main/CONTRIBUTING.md).
|
|
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)
|
|
159
138
|
|
|
160
139
|
## License
|
|
161
140
|
|
|
162
|
-
MIT.
|
|
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.
|
|
4
|
-
"description": "
|
|
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": "
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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)
|
|
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 ${
|
|
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(
|
|
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 ??
|
|
710
|
-
const requested = Number.isFinite(requestedChars) ? Math.floor(requestedChars) :
|
|
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
|
|
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(
|
|
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 }],
|