@dumbmice/pi-deepseek-websearch 0.1.4 → 0.1.5
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 +6 -4
- package/CHANGELOG.md +10 -1
- package/README.md +5 -3
- package/docs/docs-alignment.md +6 -3
- package/package.json +1 -1
- package/src/index.ts +59 -40
package/AGENTS.md
CHANGED
|
@@ -45,10 +45,12 @@ README text, the matching `docs/docs-alignment.md` entry (or marking that item s
|
|
|
45
45
|
no-runtime-dependencies rule. Do not replace them with `Type.Object` imports.
|
|
46
46
|
- **A fitting query is never rewritten.** `fitQuery` returns the caller's text untouched when it fits,
|
|
47
47
|
apart from stripped leading/trailing whitespace. Only an over-long query is tokenised and trimmed.
|
|
48
|
-
- **
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
- **Filters are constraints; words are expendable.** When the assembled query exceeds 128 characters,
|
|
49
|
+
keep every filter and remove trailing words; cut one indivisible token by code point only if it
|
|
50
|
+
alone exceeds the remaining budget. If the filters leave no room for search text, fail before the
|
|
51
|
+
request. Drop a dangling operator or unmatched quote, and record every removal in `dropped`.
|
|
52
|
+
Whitespace stripped from `query`, `ori_question` and `localization` before use is silent
|
|
53
|
+
(register item 3).
|
|
52
54
|
- **Cost follows the raw response.** `estimatedCostUsd` is keyed to `rawResults.length`, not to what
|
|
53
55
|
survives client-side filtering, because DeepSeek bills a non-empty result set.
|
|
54
56
|
- **A malformed 200 is an error.** A body without a `results` array is rejected; an empty array is a
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ All notable changes to this extension are documented in this file. The format fo
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.5] - 2026-11-06
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Over-length queries no longer lose their filters. Trimming keeps `allowed_domains`, `blocked_domains`,
|
|
14
|
+
`lang` and `recency` and removes trailing words instead; a filter set that leaves no room for search
|
|
15
|
+
text fails before the request rather than sending the query with those filters stripped.
|
|
16
|
+
|
|
9
17
|
## [0.1.4] - 2026-10-30
|
|
10
18
|
|
|
11
19
|
### Added
|
|
@@ -72,7 +80,8 @@ results are not billed. Validated against the DeepSeek API documentation as of 2
|
|
|
72
80
|
- `deepseek_fetch` rejects local and private targets and never unescapes inside code.
|
|
73
81
|
|
|
74
82
|
|
|
75
|
-
[Unreleased]: https://github.com/DumbMice/pi-deepseek-websearch/compare/v0.1.
|
|
83
|
+
[Unreleased]: https://github.com/DumbMice/pi-deepseek-websearch/compare/v0.1.5...HEAD
|
|
84
|
+
[0.1.5]: https://github.com/DumbMice/pi-deepseek-websearch/compare/v0.1.4...v0.1.5
|
|
76
85
|
[0.1.4]: https://github.com/DumbMice/pi-deepseek-websearch/compare/v0.1.3...v0.1.4
|
|
77
86
|
[0.1.3]: https://github.com/DumbMice/pi-deepseek-websearch/compare/v0.1.2...v0.1.3
|
|
78
87
|
[0.1.2]: https://github.com/DumbMice/pi-deepseek-websearch/compare/v0.1.1...v0.1.2
|
package/README.md
CHANGED
|
@@ -104,9 +104,11 @@ an error, not as an empty success.
|
|
|
104
104
|
|
|
105
105
|
## Behaviour to expect
|
|
106
106
|
|
|
107
|
-
- **Long queries are trimmed, not rejected.** The 128-character cap includes operators
|
|
108
|
-
is shortened,
|
|
109
|
-
|
|
107
|
+
- **Long queries are trimmed, not rejected.** The 128-character cap includes operators. When a query
|
|
108
|
+
is shortened, trailing words are removed and the filters are kept (a single word or quoted
|
|
109
|
+
phrase longer than the remaining room is cut); the reply names what went and prints the final query. If the filters alone
|
|
110
|
+
leave no room for search text, the call fails before any request — shorten the domain list or
|
|
111
|
+
remove a filter to send it.
|
|
110
112
|
- **Transient failures are retried.** `408`, `429`, any `5xx`, network failures and timeouts get up
|
|
111
113
|
to three attempts, honouring `retry-after` (seconds or an HTTP date) capped at 5 seconds. Every
|
|
112
114
|
other `4xx` fails immediately with DeepSeek's own message. Each attempt has its own deadline: 30
|
package/docs/docs-alignment.md
CHANGED
|
@@ -11,8 +11,9 @@ extension can emit is always a valid request.
|
|
|
11
11
|
Client-side deviations and transport details, listed in full. Items that produce no signal in the
|
|
12
12
|
output are marked _(silent)_; everything else is observable in the reply or in `details`.
|
|
13
13
|
|
|
14
|
-
1. over-length queries are trimmed rather than rejected, and every removal is reported
|
|
15
|
-
token longer than the
|
|
14
|
+
1. over-length queries are trimmed rather than rejected, and every removal is reported. Filters are
|
|
15
|
+
kept and trailing words are removed; a single token longer than the remaining budget is cut
|
|
16
|
+
mid-token. If the filters alone leave no room for search text, the call fails before the request;
|
|
16
17
|
2. query whitespace is normalised only when the query must be rewritten, and that is reported in the
|
|
17
18
|
same line;
|
|
18
19
|
3. leading and trailing whitespace is stripped from `query`, `ori_question` and `localization` before
|
|
@@ -63,7 +64,9 @@ output are marked _(silent)_; everything else is observable in the reply or in `
|
|
|
63
64
|
23. result fields of an unexpected type are ignored rather than passed through, so one malformed
|
|
64
65
|
result degrades instead of failing the call _(silent)_;
|
|
65
66
|
24. `recency` windows are fixed client-side at 1 / 7 / 30 / 365 days for `day` / `week` / `month` /
|
|
66
|
-
`year
|
|
67
|
+
`year`;
|
|
68
|
+
25. domain list entries that are empty or contain whitespace or a quote are dropped before the query is
|
|
69
|
+
assembled _(silent)_.
|
|
67
70
|
|
|
68
71
|
Transport details: requests send `Accept: application/json` alongside the two documented headers;
|
|
69
72
|
retry backoff is 500 ms then 1 s when no `retry-after` is present (two sleeps for three attempts); and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dumbmice/pi-deepseek-websearch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
package/src/index.ts
CHANGED
|
@@ -242,10 +242,9 @@ async function postDeepSeek<T>(
|
|
|
242
242
|
/* ------------------------------------------------------------------- query */
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
|
-
*
|
|
246
|
-
* operators). Filters are
|
|
247
|
-
*
|
|
248
|
-
* the request differs from what was asked for.
|
|
245
|
+
* Fit the assembled query into the API's hard limit (docs: 1-128 Unicode characters,
|
|
246
|
+
* operators included). Filters are explicit constraints, so they are kept and the
|
|
247
|
+
* free-text part is shortened instead. Every removal is reported.
|
|
249
248
|
*/
|
|
250
249
|
export function fitQuery(options: SearchOptions): { query: string; dropped: string[] } {
|
|
251
250
|
const dropped: string[] = [];
|
|
@@ -272,33 +271,36 @@ export function fitQuery(options: SearchOptions): { query: string; dropped: stri
|
|
|
272
271
|
const untouched = [raw, ...filters].filter(Boolean).join(" ");
|
|
273
272
|
if (charLength(untouched) <= MAX_QUERY_CHARS) return { query: untouched, dropped };
|
|
274
273
|
|
|
275
|
-
//
|
|
274
|
+
// The filters occupy the tail of the string, so they set the budget for the words.
|
|
275
|
+
const filterText = filters.join(" ");
|
|
276
|
+
const filterCost = filterText ? charLength(filterText) + 1 : 0;
|
|
277
|
+
const available = MAX_QUERY_CHARS - filterCost;
|
|
278
|
+
if (available < 1) {
|
|
279
|
+
const names = [
|
|
280
|
+
options.allowedDomains.length > 0 ? "allowed_domains" : "",
|
|
281
|
+
options.blockedDomains.length > 0 ? "blocked_domains" : "",
|
|
282
|
+
options.lang ? "lang" : "",
|
|
283
|
+
options.recency ? "recency" : "",
|
|
284
|
+
].filter(Boolean);
|
|
285
|
+
const named =
|
|
286
|
+
names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}` : names[0]!;
|
|
287
|
+
throw new Error(
|
|
288
|
+
`The ${named} ${names.length > 1 ? "filters need" : "filter needs"} ${filterCost} characters ` +
|
|
289
|
+
"of the 128-character query limit, leaving no room for the search text. " +
|
|
290
|
+
"Shorten the filter values or remove a filter.",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
276
294
|
// An unescaped quoted phrase is one token: slicing mid-phrase would change its meaning.
|
|
277
295
|
// Backslash-escaped quotes are not special-cased (only reachable on over-length queries).
|
|
278
296
|
const matchedTokens = raw.match(/"[^"]*"|\S+/g);
|
|
279
|
-
|
|
297
|
+
let words: string[] = matchedTokens ? [...matchedTokens] : [raw];
|
|
280
298
|
const quotesWereBalanced = ((raw.match(/"/g) ?? []).length % 2) === 0;
|
|
281
|
-
if (raw !==
|
|
282
|
-
let words = tokens;
|
|
283
|
-
let active = [...filters];
|
|
284
|
-
|
|
285
|
-
const overBudget = () => charLength(join(words, active)) > MAX_QUERY_CHARS;
|
|
286
|
-
const dropFilter = (predicate: (filter: string) => boolean, label: (filter: string) => string): void => {
|
|
287
|
-
const index = active.findIndex(predicate);
|
|
288
|
-
if (index < 0 || !overBudget()) return;
|
|
289
|
-
const [removed] = active.splice(index, 1);
|
|
290
|
-
dropped.push(label(removed!));
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
dropFilter((f) => f.startsWith("after:"), (f) => `recency (${f})`);
|
|
294
|
-
dropFilter((f) => f.startsWith("lang:"), (f) => `language (${f})`);
|
|
295
|
-
while (overBudget() && active.length > 0) {
|
|
296
|
-
const removed = active.pop()!;
|
|
297
|
-
dropped.push(`domain filter (${removed.slice(0, 40)})`);
|
|
298
|
-
}
|
|
299
|
+
if (raw !== words.join(" ")) dropped.push("query whitespace normalised");
|
|
299
300
|
|
|
300
301
|
const wordsBefore = words.join(" ");
|
|
301
|
-
while (words.length > 1 &&
|
|
302
|
+
while (words.length > 1 && charLength(words.join(" ")) > available) words = words.slice(0, -1);
|
|
303
|
+
|
|
302
304
|
// Never leave a dangling operator (`site:`, `-site:`, `after:`, a bare `-`) or an
|
|
303
305
|
// unbalanced quote behind: DeepSeek would answer 400 and no retry would help.
|
|
304
306
|
const danglingOperator = /^(?:-|-[A-Za-z]+:|[A-Za-z]+:)$/;
|
|
@@ -313,20 +315,37 @@ export function fitQuery(options: SearchOptions): { query: string; dropped: stri
|
|
|
313
315
|
dropped.push("trailing query words");
|
|
314
316
|
}
|
|
315
317
|
|
|
316
|
-
let query = join(words,
|
|
318
|
+
let query = join(words, filters);
|
|
317
319
|
if (charLength(query) > MAX_QUERY_CHARS) {
|
|
318
|
-
//
|
|
319
|
-
// by code point
|
|
320
|
-
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
320
|
+
// One indivisible token longer than the remaining budget: cutting it is unavoidable,
|
|
321
|
+
// so do that by code point rather than dropping a filter.
|
|
322
|
+
let cut = Array.from(words.join(" ")).slice(0, available).join("").trim();
|
|
323
|
+
// Repeat until stable: splicing an unmatched quote can expose a trailing `-`/`:`.
|
|
324
|
+
for (let changed = true; changed; ) {
|
|
325
|
+
changed = false;
|
|
326
|
+
const stripped = cut.replace(/[-:]+$/, "").trimEnd();
|
|
327
|
+
if (stripped !== cut) {
|
|
328
|
+
cut = stripped;
|
|
329
|
+
changed = true;
|
|
330
|
+
}
|
|
331
|
+
if (quotesWereBalanced && (cut.match(/"/g) ?? []).length % 2 === 1) {
|
|
332
|
+
const balanced = Array.from(cut);
|
|
333
|
+
const lastQuote = balanced.lastIndexOf('"');
|
|
334
|
+
if (lastQuote >= 0) balanced.splice(lastQuote, 1);
|
|
335
|
+
const next = balanced.join("").trimEnd();
|
|
336
|
+
if (next !== cut) {
|
|
337
|
+
cut = next;
|
|
338
|
+
changed = true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (cut === "" && filters.length === 0) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
"The query has no searchable text within the 128-character limit — it consists only of " +
|
|
345
|
+
"operators or punctuation. Add search terms.",
|
|
346
|
+
);
|
|
329
347
|
}
|
|
348
|
+
query = join([cut], filters);
|
|
330
349
|
dropped.push("query tail");
|
|
331
350
|
}
|
|
332
351
|
return { query, dropped };
|
|
@@ -502,7 +521,7 @@ const SEARCH_SCHEMA = {
|
|
|
502
521
|
type: "string",
|
|
503
522
|
minLength: 1,
|
|
504
523
|
description:
|
|
505
|
-
'Search query. Supports operators: "exact phrase", -word, -"phrase", site:a.com OR site:b.com, -site:c.com, lang:en,zh, after:2025-01-01, before:2026-06-01. Queries are capped at 128 chars including operators; anything longer is trimmed automatically (
|
|
524
|
+
'Search query. Supports operators: "exact phrase", -word, -"phrase", site:a.com OR site:b.com, -site:c.com, lang:en,zh, after:2025-01-01, before:2026-06-01. Queries are capped at 128 chars including operators; anything longer is trimmed automatically (trailing words are removed, filters are kept) and the removals are reported in the output — do not pre-shorten.',
|
|
506
525
|
},
|
|
507
526
|
count: {
|
|
508
527
|
type: "integer",
|
|
@@ -524,13 +543,13 @@ const SEARCH_SCHEMA = {
|
|
|
524
543
|
type: "array",
|
|
525
544
|
items: { type: "string" },
|
|
526
545
|
description:
|
|
527
|
-
"Restrict results to these domains (compiled into site: OR site:). Counts toward the 128-char query cap
|
|
546
|
+
"Restrict results to these domains (compiled into site: OR site:). Counts toward the 128-char query cap and is never dropped by trimming; if the filters leave no room for the query text, the call fails before any request.",
|
|
528
547
|
},
|
|
529
548
|
blocked_domains: {
|
|
530
549
|
type: "array",
|
|
531
550
|
items: { type: "string" },
|
|
532
551
|
description:
|
|
533
|
-
"Exclude these domains (compiled into -site:). Counts toward the 128-char query cap and
|
|
552
|
+
"Exclude these domains (compiled into -site:). Counts toward the 128-char query cap and is never dropped by trimming; if the filters leave no room for the query text, the call fails before any request.",
|
|
534
553
|
},
|
|
535
554
|
lang: { type: "string", description: 'Language codes, e.g. "zh,en" (becomes lang:).' },
|
|
536
555
|
recency: {
|