@sdsrs/llm-wiki 0.9.0 → 0.9.1
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/CHANGELOG.md +20 -0
- package/bin/llm-wiki.mjs +2 -1
- package/package.json +1 -1
- package/src/embed.mjs +26 -4
- package/src/local-embed.mjs +13 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.1 (2026-07-12)
|
|
4
|
+
|
|
5
|
+
Correctness and diagnostics for the 0.9.0 local-embedding path; no change to the
|
|
6
|
+
vectors produced. Suite 271 → 272.
|
|
7
|
+
|
|
8
|
+
**Fixed:**
|
|
9
|
+
|
|
10
|
+
- **Long pages no longer truncate silently on local models.** A local model
|
|
11
|
+
(e.g. `local:Xenova/multilingual-e5-small`) has a ~512-token input window and its
|
|
12
|
+
pipeline silently truncates longer pages to a head-only vector. `embed` now warns per
|
|
13
|
+
page and reports a `truncated N` count in its summary, so the head-only truncation is
|
|
14
|
+
visible instead of silent. The vector produced is unchanged.
|
|
15
|
+
- **A broken transitive dependency is no longer masked as "install
|
|
16
|
+
@huggingface/transformers".** The missing-dependency hint now fires only when
|
|
17
|
+
transformers.js itself is absent; when transformers.js is installed but one of its
|
|
18
|
+
(native) deps fails to load, the original error naming the real culprit passes through.
|
|
19
|
+
- **Clearer `embed` error when no embedding model is configured** — it now names the
|
|
20
|
+
actual fix (set `embeddingModel`) for both remote and local, instead of telling a user
|
|
21
|
+
who already has chat credentials to add credentials.
|
|
22
|
+
|
|
3
23
|
## 0.9.0 (2026-07-12)
|
|
4
24
|
|
|
5
25
|
**Added:**
|
package/bin/llm-wiki.mjs
CHANGED
|
@@ -77,7 +77,8 @@ program.command('embed')
|
|
|
77
77
|
.option('--kb <dir>', 'knowledge base root', '.')
|
|
78
78
|
.action(async (opts) => {
|
|
79
79
|
const r = await embedKb(opts.kb)
|
|
80
|
-
|
|
80
|
+
const trunc = r.truncated ? `, truncated ${r.truncated}` : ''
|
|
81
|
+
console.log(`embedded ${r.embedded}, reused ${r.reused}, pruned ${r.pruned}${trunc} (model ${r.model}, dim ${r.dim})`)
|
|
81
82
|
})
|
|
82
83
|
|
|
83
84
|
program.command('ask <question>')
|
package/package.json
CHANGED
package/src/embed.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { loadEmbedConfig, makeTransport } from './llm-config.mjs'
|
|
|
3
3
|
import { sha256Text } from './hashing.mjs'
|
|
4
4
|
import { normalize, pageEmbedText, loadVectorStore, saveVectorStore } from './vector.mjs'
|
|
5
5
|
import { fetchWithRetry } from './retry.mjs'
|
|
6
|
+
import { estimateTokens } from './scanner.mjs'
|
|
6
7
|
import { embedLocal, isLocalModel, stripLocalPrefix } from './local-embed.mjs'
|
|
7
8
|
|
|
8
9
|
const BATCH = 64
|
|
@@ -11,6 +12,15 @@ const BATCH = 64
|
|
|
11
12
|
// whole-page retrieval is unaffected — the page file and its BM25 text are never
|
|
12
13
|
// touched, only the vector's input is truncated so one huge page can't abort the run.
|
|
13
14
|
const EMBED_TOKEN_CAP = 8000
|
|
15
|
+
// Local (transformers.js) models don't reject over-limit input the way an API does —
|
|
16
|
+
// the feature-extraction pipeline silently truncates at the model's own window, so a
|
|
17
|
+
// page over it yields a head-only vector with NO error. e5-small and most Xenova
|
|
18
|
+
// sentence-transformers are 512 tokens; we keep the 8000 cap on the text we send (it
|
|
19
|
+
// bounds compute and stays well above the window so the pipeline still fills it) and
|
|
20
|
+
// warn separately, via a realistic token estimate, whenever a page crosses this window
|
|
21
|
+
// so the head-only truncation is visible instead of silent. A model with a different
|
|
22
|
+
// window would extend this into a per-model lookup.
|
|
23
|
+
const LOCAL_TOKEN_WINDOW = 512
|
|
14
24
|
|
|
15
25
|
// Worst-case token count for the embedding API — deliberately NOT scanner's
|
|
16
26
|
// estimateTokens (chars/4), which underestimates dense markdown/code where real
|
|
@@ -76,20 +86,32 @@ export async function embedTexts(cfg, t, texts, { role = 'passage' } = {}) {
|
|
|
76
86
|
|
|
77
87
|
export async function embedKb(kbRoot, { fetchImpl, retry, pipelineFactory } = {}) {
|
|
78
88
|
const cfg = loadEmbedConfig(kbRoot)
|
|
79
|
-
if (!cfg?.embeddingModel) throw new Error('No
|
|
89
|
+
if (!cfg?.embeddingModel) throw new Error('No embedding model configured. Set "embeddingModel" in ~/.llm-wiki/config.json — a remote one (e.g. "text-embedding-3-small") also needs the provider baseURL/apiKey; a local one (e.g. "local:Xenova/multilingual-e5-small") needs no credentials.')
|
|
90
|
+
const local = isLocalModel(cfg.embeddingModel)
|
|
80
91
|
const pages = listWikiPages(kbRoot).filter(p => !p.error && !isInvalidated(p))
|
|
81
92
|
const prev = loadVectorStore(kbRoot)
|
|
82
93
|
const reuse = (prev && prev.model === cfg.embeddingModel) ? prev.pages : {}
|
|
83
94
|
const jobs = []
|
|
84
95
|
const nextPages = {}
|
|
85
96
|
let reused = 0
|
|
97
|
+
let truncatedCount = 0
|
|
86
98
|
for (const pg of pages) {
|
|
87
99
|
const raw = pageEmbedText(pg)
|
|
88
100
|
const text = capEmbedText(raw)
|
|
89
|
-
|
|
101
|
+
// API path: `truncated` means our worst-case cap actually cut the text (the API
|
|
102
|
+
// would otherwise reject it). Local path: the cap rarely fires (window ≪ 8000), so
|
|
103
|
+
// detect the model's OWN silent truncation with a realistic estimate against its
|
|
104
|
+
// window — that's what produces a head-only vector the user should know about.
|
|
105
|
+
const overWindow = local && estimateTokens(raw) > LOCAL_TOKEN_WINDOW
|
|
106
|
+
const truncated = overWindow || (!local && text !== raw)
|
|
90
107
|
const hash = sha256Text(text)
|
|
91
108
|
if (reuse[pg.relPath]?.hash === hash) { nextPages[pg.relPath] = reuse[pg.relPath]; reused++; continue }
|
|
92
|
-
if (truncated)
|
|
109
|
+
if (truncated) {
|
|
110
|
+
truncatedCount++
|
|
111
|
+
process.stderr.write(overWindow
|
|
112
|
+
? `warning: ${pg.relPath} exceeds the local embedding model's ~${LOCAL_TOKEN_WINDOW}-token window — its vector covers only the page head\n`
|
|
113
|
+
: `warning: ${pg.relPath} embed text truncated to ~${EMBED_TOKEN_CAP} worst-case tokens (page exceeds embedding model input limit)\n`)
|
|
114
|
+
}
|
|
93
115
|
jobs.push({ relPath: pg.relPath, text, hash })
|
|
94
116
|
}
|
|
95
117
|
let dim = (prev && prev.model === cfg.embeddingModel) ? prev.dim : null
|
|
@@ -119,5 +141,5 @@ export async function embedKb(kbRoot, { fetchImpl, retry, pipelineFactory } = {}
|
|
|
119
141
|
const pruned = prev ? Object.keys(prev.pages).filter(id => !(id in nextPages)).length : 0
|
|
120
142
|
const store = { model: cfg.embeddingModel, dim: dim ?? 0, pages: nextPages }
|
|
121
143
|
saveVectorStore(kbRoot, store)
|
|
122
|
-
return { embedded, reused, pruned, model: cfg.embeddingModel, dim: store.dim }
|
|
144
|
+
return { embedded, reused, pruned, truncated: truncatedCount, model: cfg.embeddingModel, dim: store.dim }
|
|
123
145
|
}
|
package/src/local-embed.mjs
CHANGED
|
@@ -16,9 +16,20 @@ export function stripLocalPrefix(m) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
// A missing optional dependency surfaces as ERR_MODULE_NOT_FOUND — turn it into an
|
|
19
|
-
// actionable instruction instead of an opaque stack.
|
|
19
|
+
// actionable instruction instead of an opaque stack. Only map it when the ABSENT
|
|
20
|
+
// module is transformers.js itself: the same code fires when transformers.js IS
|
|
21
|
+
// installed but one of its (transitive/native) deps fails to resolve, and telling the
|
|
22
|
+
// user to re-install transformers then hides the real missing module. Pass those
|
|
23
|
+
// through so their message names the actual culprit.
|
|
20
24
|
export function friendlyImportError(err) {
|
|
21
|
-
if (err?.code
|
|
25
|
+
if (err?.code !== 'ERR_MODULE_NOT_FOUND') return err
|
|
26
|
+
// Gate on the MISSING package — the quoted name after "Cannot find package/module" —
|
|
27
|
+
// NOT a substring of the whole message. A transitive failure names its real culprit
|
|
28
|
+
// in the quotes (e.g. 'onnxruntime-node') yet still mentions "@huggingface/transformers"
|
|
29
|
+
// in the "imported from <path>" tail, so a whole-message test mis-maps it. Rewrite only
|
|
30
|
+
// when the absent module IS transformers.js (the package root or a subpath import of it).
|
|
31
|
+
const missing = /Cannot find (?:package|module) '([^']+)'/.exec(err.message ?? '')?.[1]
|
|
32
|
+
if (missing && /^@huggingface\/transformers(\/|$)/.test(missing)) {
|
|
22
33
|
return new Error('local embedding needs @huggingface/transformers; run: npm i @huggingface/transformers')
|
|
23
34
|
}
|
|
24
35
|
return err
|