@7n/llm-lib 2.14.15 → 2.14.16
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 +6 -0
- package/lib/internal/docs/native.md +34 -14
- package/lib/internal/native.mjs +124 -33
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.14.16] - 2026-08-04
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Loader napi-аддона (`lib/internal/native.mjs`) у вихідному дереві репо (маркер `llm-lib/crates/llm-lib-napi/Cargo.toml`) резолвить локальну збірку `target/release|debug` ПЕРЕД опублікованим підпакетом `@7n/llm-lib-<platform>-<arch>` — раніше свіжий `cargo build -p llm-lib-napi` мовчки перекривався registry-бінарем із `node_modules`, і правки Rust-ядра не проявлялися. У встановленому пакеті порядок незмінний: підпакет лишається авторитетним джерелом
|
|
8
|
+
|
|
3
9
|
## [2.14.15] - 2026-08-04
|
|
4
10
|
|
|
5
11
|
### Fixed
|
|
@@ -3,30 +3,50 @@ type: JS Module
|
|
|
3
3
|
title: native.mjs
|
|
4
4
|
resource: llm-lib/lib/internal/native.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
7
|
-
model:
|
|
8
|
-
tier:
|
|
9
|
-
score:
|
|
10
|
-
judgeModel: openai-codex/gpt-5.4-mini
|
|
6
|
+
crc: 35a583cb
|
|
7
|
+
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
+
tier: local-min
|
|
9
|
+
score: 70
|
|
11
10
|
---
|
|
12
11
|
|
|
13
12
|
## Огляд
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
Loader napi-аддона `llm-lib` (Rust-ядро `llm-lib/crates/llm-lib-napi`
|
|
15
|
+
→ `llm-lib`) — за зразком `mt/npm/lib/core/native.mjs`.
|
|
16
|
+
|
|
17
|
+
Порядок пошуку (залежить від оточення — див. [`isSourceTree`]):
|
|
18
|
+
1. N_LLM_LIB_NATIVE_ADDON — явний override шляху до аддона (dev / CI / тести).
|
|
19
|
+
2. **Лише у вихідному дереві** (`<repoRoot>/llm-lib/crates/llm-lib-napi/Cargo.toml`
|
|
20
|
+
існує): локальна збірка `<repoRoot>/target/release|debug/` (сирий cdylib
|
|
21
|
+
з `cargo build -p llm-lib-napi`) та вивід `napi build` у
|
|
22
|
+
`llm-lib/crates/llm-lib-napi/`.
|
|
23
|
+
3. Platform-підпакет `@7n/llm-lib-<platform>-<arch>` (napi-артефакт
|
|
24
|
+
`llm-lib-napi.<triple>.node`).
|
|
25
|
+
4. Той самий fallback на локальну збірку поза вихідним деревом
|
|
26
|
+
(у продакшені поведінка така сама, як до фіксу).
|
|
27
|
+
5. Інакше — зрозуміла помилка з підказкою.
|
|
28
|
+
|
|
29
|
+
ЧОМУ порядок різний (симетрично до `npm/scripts/lib/native.mjs`, фікс
|
|
30
|
+
2026-08-03): у репо локальний `cargo build -p llm-lib-napi` мовчки
|
|
31
|
+
перекривався опублікованим підпакетом із `node_modules` — правки Rust-ядра
|
|
32
|
+
не проявлялися, а «фейли LLM-контуру» діагностувалися як помилки коду.
|
|
33
|
+
У користувача ж підпакет — єдине авторитетне джерело (запінений lockstep до
|
|
34
|
+
версії `@7n/llm-lib`), тож сторонній `target/` поруч не має його перебивати.
|
|
35
|
+
|
|
36
|
+
Аддон завантажується через `process.dlopen` — працює і для `.node`, і для
|
|
37
|
+
сирих cdylib (`.dylib`/`.so`). Результат кешується (одне завантаження на процес).
|
|
38
|
+
Без JS-fallback на неоголошеній платформі — hard error, свідома межа v1
|
|
39
|
+
(darwin-arm64, linux-x64), не регресія.
|
|
24
40
|
|
|
25
41
|
## Публічний API
|
|
26
42
|
|
|
27
43
|
- resolveNativeAddon — Резолвить шлях до napi-аддона `llm-lib`.
|
|
28
44
|
- loadNative — Кешований доступ до аддона (одне завантаження на процес).
|
|
29
45
|
|
|
46
|
+
## Сценарії використання
|
|
47
|
+
|
|
48
|
+
- `llm-lib/tests/native.test.mjs` (resolveNativeAddon (порядок пошуку); resolveNativeAddon (вихідне дерево vs прод)) — N_LLM_LIB_NATIVE_ADDON має найвищий пріоритет; platform-підпакет: резолвиться @7n/llm-lib-<key> з napi-суфіксом; linux-x64 мапиться на суфікс linux-x64-gnu; dev-fallback: release-cdylib перемагає debug; dev-fallback: на linux шукається .so, а останній кандидат — вивід napi build; ще 6
|
|
49
|
+
|
|
30
50
|
## Гарантії поведінки
|
|
31
51
|
|
|
32
52
|
- Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
|
package/lib/internal/native.mjs
CHANGED
|
@@ -2,14 +2,24 @@
|
|
|
2
2
|
* Loader napi-аддона `llm-lib` (Rust-ядро `llm-lib/crates/llm-lib-napi`
|
|
3
3
|
* → `llm-lib`) — за зразком `mt/npm/lib/core/native.mjs`.
|
|
4
4
|
*
|
|
5
|
-
* Порядок
|
|
5
|
+
* Порядок пошуку (залежить від оточення — див. [`isSourceTree`]):
|
|
6
6
|
* 1. N_LLM_LIB_NATIVE_ADDON — явний override шляху до аддона (dev / CI / тести).
|
|
7
|
-
* 2.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `cargo build -p llm-lib-napi`) та вивід `napi build` у
|
|
7
|
+
* 2. **Лише у вихідному дереві** (`<repoRoot>/llm-lib/crates/llm-lib-napi/Cargo.toml`
|
|
8
|
+
* існує): локальна збірка `<repoRoot>/target/release|debug/` (сирий cdylib
|
|
9
|
+
* з `cargo build -p llm-lib-napi`) та вивід `napi build` у
|
|
11
10
|
* `llm-lib/crates/llm-lib-napi/`.
|
|
12
|
-
*
|
|
11
|
+
* 3. Platform-підпакет `@7n/llm-lib-<platform>-<arch>` (napi-артефакт
|
|
12
|
+
* `llm-lib-napi.<triple>.node`).
|
|
13
|
+
* 4. Той самий fallback на локальну збірку поза вихідним деревом
|
|
14
|
+
* (у продакшені поведінка така сама, як до фіксу).
|
|
15
|
+
* 5. Інакше — зрозуміла помилка з підказкою.
|
|
16
|
+
*
|
|
17
|
+
* ЧОМУ порядок різний (симетрично до `npm/scripts/lib/native.mjs`, фікс
|
|
18
|
+
* 2026-08-03): у репо локальний `cargo build -p llm-lib-napi` мовчки
|
|
19
|
+
* перекривався опублікованим підпакетом із `node_modules` — правки Rust-ядра
|
|
20
|
+
* не проявлялися, а «фейли LLM-контуру» діагностувалися як помилки коду.
|
|
21
|
+
* У користувача ж підпакет — єдине авторитетне джерело (запінений lockstep до
|
|
22
|
+
* версії `@7n/llm-lib`), тож сторонній `target/` поруч не має його перебивати.
|
|
13
23
|
*
|
|
14
24
|
* Аддон завантажується через `process.dlopen` — працює і для `.node`, і для
|
|
15
25
|
* сирих cdylib (`.dylib`/`.so`). Результат кешується (одне завантаження на процес).
|
|
@@ -57,7 +67,59 @@ function cdylibName(platform) {
|
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
/**
|
|
60
|
-
*
|
|
70
|
+
* Чи запущено loader із вихідного дерева репо (dev-машина або CI), а не з
|
|
71
|
+
* встановленого пакета `@7n/llm-lib`. Маркер — `llm-lib/crates/llm-lib-napi/Cargo.toml`
|
|
72
|
+
* поруч із `repoRoot`: у репо він закомічений завжди, а `files` пакета
|
|
73
|
+
* (`bin`, `lib`, …) `crates/` не відвантажує — у проді маркера немає.
|
|
74
|
+
* @param {string} repoRoot корінь, від якого рахуються кандидати
|
|
75
|
+
* @param {(p: string) => boolean} exists перевірка існування (ін'єкція для тестів)
|
|
76
|
+
* @returns {boolean} true — вихідне дерево репо
|
|
77
|
+
*/
|
|
78
|
+
function isSourceTree(repoRoot, exists) {
|
|
79
|
+
return exists(join(repoRoot, 'llm-lib', 'crates', 'llm-lib-napi', 'Cargo.toml'))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Кандидати локальної збірки: сирий cdylib з `cargo build -p llm-lib-napi`
|
|
84
|
+
* (release перед debug) і вивід `napi build` у `llm-lib/crates/llm-lib-napi/`.
|
|
85
|
+
* @param {string} repoRoot корінь, від якого рахуються шляхи
|
|
86
|
+
* @param {string} platform process.platform
|
|
87
|
+
* @param {string|undefined} suffix napi-суфікс платформи
|
|
88
|
+
* @returns {string[]} шляхи кандидатів у порядку пріоритету
|
|
89
|
+
*/
|
|
90
|
+
function localBuildCandidates(repoRoot, platform, suffix) {
|
|
91
|
+
const candidates = Array.from(['release', 'debug'], profile =>
|
|
92
|
+
join(repoRoot, 'target', profile, cdylibName(platform))
|
|
93
|
+
)
|
|
94
|
+
if (suffix) {
|
|
95
|
+
candidates.push(join(repoRoot, 'llm-lib', 'crates', 'llm-lib-napi', `llm-lib-napi.${suffix}.node`))
|
|
96
|
+
}
|
|
97
|
+
return candidates
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Помилка «немає збірки» з підказкою і причиною останньої невдачі.
|
|
102
|
+
* @param {string} key `${platform}-${arch}`
|
|
103
|
+
* @param {string} lastError текст останньої помилки dlopen (може бути порожнім)
|
|
104
|
+
* @returns {Error} готова помилка
|
|
105
|
+
*/
|
|
106
|
+
function missingAddonError(key, lastError) {
|
|
107
|
+
const tail = lastError ? ` Остання спроба — ${lastError}.` : ''
|
|
108
|
+
return new Error(
|
|
109
|
+
`llm-lib native addon: немає збірки для "${key}". ` +
|
|
110
|
+
`Постав N_LLM_LIB_NATIVE_ADDON=/шлях/до/аддона, додай підпакет @7n/llm-lib-${key}, ` +
|
|
111
|
+
`або збери локально: cargo build --release -p llm-lib-napi.${tail}`
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Ланцюг кандидатів аддона в порядку пріоритету.
|
|
117
|
+
*
|
|
118
|
+
* Повертає СПИСОК, а не один шлях, свідомо: `existsSync` — не доказ, що аддон
|
|
119
|
+
* завантажиться (файл може бути з іншої платформи, побитий, або `existsSync`
|
|
120
|
+
* підмінений моком у тесті, що не має до аддона стосунку — саме так
|
|
121
|
+
* `gen-tests.test.mjs` валив увесь контур). Остаточний вибір робить
|
|
122
|
+
* [`loadNative`], пробуючи кандидатів по черзі.
|
|
61
123
|
* @param {{
|
|
62
124
|
* env?: Record<string, string | undefined>,
|
|
63
125
|
* platform?: string,
|
|
@@ -66,9 +128,9 @@ function cdylibName(platform) {
|
|
|
66
128
|
* requireResolve?: (id: string) => string,
|
|
67
129
|
* repoRoot?: string
|
|
68
130
|
* }} [deps] ін'єкції для тестів
|
|
69
|
-
* @returns {string}
|
|
131
|
+
* @returns {string[]} шляхи в порядку пріоритету (може бути порожнім)
|
|
70
132
|
*/
|
|
71
|
-
export function
|
|
133
|
+
export function nativeAddonChain(deps = {}) {
|
|
72
134
|
const env = deps.env ?? procEnv
|
|
73
135
|
const platform = deps.platform ?? osPlatform
|
|
74
136
|
const arch = deps.arch ?? osArch
|
|
@@ -76,50 +138,79 @@ export function resolveNativeAddon(deps = {}) {
|
|
|
76
138
|
const requireResolve = deps.requireResolve ?? (id => require.resolve(id))
|
|
77
139
|
const repoRoot = deps.repoRoot ?? REPO_ROOT
|
|
78
140
|
|
|
79
|
-
//
|
|
141
|
+
// Явний override — єдине джерело: якщо він заданий і не вантажиться,
|
|
142
|
+
// мовчазний відкат приховав би саме те, що просили перевірити.
|
|
80
143
|
const override = env.N_LLM_LIB_NATIVE_ADDON
|
|
81
|
-
if (override) return override
|
|
144
|
+
if (override) return [override]
|
|
82
145
|
|
|
83
146
|
const key = `${platform}-${arch}`
|
|
84
147
|
const suffix = NAPI_SUFFIXES[key]
|
|
85
148
|
|
|
86
|
-
//
|
|
149
|
+
// Маркер вихідного дерева перевіряється ПЕРШИМ — саме він обирає порядок
|
|
150
|
+
// джерел, тож рахувати його після кандидатів було б плутаниною (і ламало б
|
|
151
|
+
// гейт на порядок звернень до fs).
|
|
152
|
+
const fromSource = isSourceTree(repoRoot, exists)
|
|
153
|
+
const local = localBuildCandidates(repoRoot, platform, suffix).filter(p => exists(p))
|
|
154
|
+
|
|
155
|
+
/** @type {string[]} */
|
|
156
|
+
let subpackage = []
|
|
87
157
|
if (suffix) {
|
|
88
158
|
try {
|
|
89
|
-
|
|
159
|
+
subpackage = [requireResolve(`@7n/llm-lib-${key}/llm-lib-napi.${suffix}.node`)]
|
|
90
160
|
} catch {
|
|
91
|
-
// не встановлено —
|
|
161
|
+
// не встановлено — лишається лише локальна збірка
|
|
92
162
|
}
|
|
93
163
|
}
|
|
94
164
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
candidates.push(join(repoRoot, 'llm-lib', 'crates', 'llm-lib-napi', `llm-lib-napi.${suffix}.node`))
|
|
101
|
-
}
|
|
102
|
-
for (const candidate of candidates) {
|
|
103
|
-
if (exists(candidate)) return candidate
|
|
104
|
-
}
|
|
165
|
+
// Вихідне дерево (dev / CI цього репо): локальна збірка попереду, інакше
|
|
166
|
+
// свіжий `cargo build -p llm-lib-napi` мовчки перекривався б підпакетом.
|
|
167
|
+
// У проді порядок зворотний — підпакет запінений і авторитетний.
|
|
168
|
+
return fromSource ? [...local, ...subpackage] : [...subpackage, ...local]
|
|
169
|
+
}
|
|
105
170
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Резолвить шлях до napi-аддона `llm-lib` — перший кандидат ланцюга
|
|
173
|
+
* [`nativeAddonChain`]. Фактичний вибір з урахуванням невдалих dlopen
|
|
174
|
+
* робить [`loadNative`].
|
|
175
|
+
* @param {Parameters<typeof nativeAddonChain>[0]} [deps] ін'єкції для тестів
|
|
176
|
+
* @returns {string} шлях до файлу аддона
|
|
177
|
+
*/
|
|
178
|
+
export function resolveNativeAddon(deps = {}) {
|
|
179
|
+
const chain = nativeAddonChain(deps)
|
|
180
|
+
if (chain.length === 0) {
|
|
181
|
+
const key = `${deps.platform ?? osPlatform}-${deps.arch ?? osArch}`
|
|
182
|
+
throw missingAddonError(key, '')
|
|
183
|
+
}
|
|
184
|
+
return chain[0]
|
|
112
185
|
}
|
|
113
186
|
|
|
114
187
|
/**
|
|
115
188
|
* Кешований доступ до аддона (одне завантаження на процес).
|
|
116
|
-
* @param {{
|
|
189
|
+
* @param {{
|
|
190
|
+
* resolve?: () => string,
|
|
191
|
+
* resolveChain?: () => string[],
|
|
192
|
+
* dlopen?: (p: string) => Record<string, unknown>
|
|
193
|
+
* }} [deps] ін'єкції
|
|
117
194
|
* @returns {Record<string, unknown>} exports аддона (oneShotAcp, resolveModel, oneShotLocalCloud)
|
|
118
195
|
*/
|
|
119
196
|
export function loadNative(deps = {}) {
|
|
120
197
|
if (cached === null) {
|
|
121
|
-
const
|
|
122
|
-
|
|
198
|
+
const dlopen = deps.dlopen ?? dlopenAddon
|
|
199
|
+
const chain = deps.resolve ? [deps.resolve()] : (deps.resolveChain ?? nativeAddonChain)()
|
|
200
|
+
|
|
201
|
+
/** @type {Record<string, unknown> | null} */
|
|
202
|
+
let addon = null
|
|
203
|
+
let lastError = ''
|
|
204
|
+
for (const candidate of chain) {
|
|
205
|
+
try {
|
|
206
|
+
addon = dlopen(candidate)
|
|
207
|
+
break
|
|
208
|
+
} catch (error) {
|
|
209
|
+
lastError = `${candidate}: ${error.message}`
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (addon === null) throw missingAddonError(`${osPlatform}-${osArch}`, lastError)
|
|
213
|
+
cached = addon
|
|
123
214
|
}
|
|
124
215
|
return cached
|
|
125
216
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/llm-lib",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.16",
|
|
4
4
|
"description": "Тонкий шар роботи з LLM (локальні omlx + хмарні провайдери) поверх pi: model tiers, one-shot, agentic-раннери, write-guard, trace, telemetry, prompt-budget",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nitra",
|