@7n/llm-lib 2.14.14 → 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 CHANGED
@@ -1,5 +1,17 @@
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
+
9
+ ## [2.14.15] - 2026-08-04
10
+
11
+ ### Fixed
12
+
13
+ - `cargo fmt` для `crates/llm-lib/src/batch.rs` — два `providers.insert(...)` у тестах local-cloud каскаду лишились неформатованими після додавання нового локального провайдера і валили `cargo fmt --all -- --check` (гейти `Lint Rust` і `rust/check` у `Lint repo-wide`)
14
+
3
15
  ## [2.14.14] - 2026-08-03
4
16
 
5
17
  ### Changed
@@ -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: 655cb048
7
- model: openai-codex/gpt-5.5
8
- tier: cloud-avg
9
- score: 100
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
- Файл підʼєднує Rust/NAPI native addon для `llm-lib` і є єдиною точкою вибору джерела: явний override через `N_LLM_LIB_NATIVE_ADDON`, платформний npm-пакет `@7n/llm-lib-<platform>-<arch>` з артефактом `llm-lib-napi.<triple>.node` або локальна dev-збірка. Публічні API `resolveNativeAddon` і `loadNative` потрібні, щоб споживачі отримували native exports з однаковою поведінкою в інсталяції, CI, тестах і локальній розробці. Завантаження виконується через `process.dlopen`, тому підтримуються і `.node`, і сирі cdylib (`.dylib`/`.so`); результат кешується як одне завантаження на процес. Непідтримані платформи завершуються зрозумілою помилкою без прихованого JS fallback.
16
-
17
- ## Поведінка
18
-
19
- `loadNative` отримує шлях від `resolveNativeAddon`, завантажує знайдений native addon і повертає його exports споживачам `llm-lib`. Результат завантаження зберігається в памʼяті процесу, тому наступні звернення повторно використовують той самий addon без нового пошуку та відкриття файлу.
20
-
21
- `resolveNativeAddon` визначає джерело native addon за єдиним порядком пріоритетів: явний шлях із середовища для dev/CI/тестів, платформний npm-підпакет для підтримуваної платформи, локальні dev-збірки Rust/NAPI, а потім помилка з інструкцією для користувача. Це дає однакову поведінку для встановленого пакета, локальної розробки й тестових сценаріїв.
22
-
23
- Якщо платформа не входить до свідомо підтриманих у v1 комбінацій, JavaScript fallback не використовується: потік завершується hard error. Це фіксує межу підтримки native-ядра замість прихованої деградації поведінки.
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
  - Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
@@ -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. Platform-підпакет `@7n/llm-lib-<platform>-<arch>` (napi-артефакт
8
- * `llm-lib-napi.<triple>.node`).
9
- * 3. Dev-fallback: `<repoRoot>/target/release|debug/` (сирий cdylib з
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
- * 4. Інакше зрозуміла помилка з підказкою.
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
- * Резолвить шлях до napi-аддона `llm-lib`.
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 resolveNativeAddon(deps = {}) {
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
- // 1. Явний override.
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
- // 2. Platform-підпакет.
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
- return requireResolve(`@7n/llm-lib-${key}/llm-lib-napi.${suffix}.node`)
159
+ subpackage = [requireResolve(`@7n/llm-lib-${key}/llm-lib-napi.${suffix}.node`)]
90
160
  } catch {
91
- // не встановлено — пробуємо dev-fallback
161
+ // не встановлено — лишається лише локальна збірка
92
162
  }
93
163
  }
94
164
 
95
- // 3. Dev-fallback: cargo-збірка (сирий cdylib) або вивід napi build.
96
- const candidates = Array.from(['release', 'debug'], profile =>
97
- join(repoRoot, 'target', profile, cdylibName(platform))
98
- )
99
- if (suffix) {
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
- // 4. Помилка з підказкою.
107
- throw new Error(
108
- `llm-lib native addon: немає збірки для "${key}". ` +
109
- `Постав N_LLM_LIB_NATIVE_ADDON=/шлях/до/аддона, додай підпакет @7n/llm-lib-${key}, ` +
110
- `або збери локально: cargo build --release -p llm-lib-napi`
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 {{ resolve?: () => string, dlopen?: (p: string) => Record<string, unknown> }} [deps] ін'єкції
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 path = (deps.resolve ?? resolveNativeAddon)()
122
- cached = (deps.dlopen ?? dlopenAddon)(path)
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.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",
@@ -56,8 +56,8 @@
56
56
  "access": "public"
57
57
  },
58
58
  "optionalDependencies": {
59
- "@7n/llm-lib-darwin-arm64": "2.14.14",
60
- "@7n/llm-lib-linux-x64": "2.14.14"
59
+ "@7n/llm-lib-darwin-arm64": "2.14.15",
60
+ "@7n/llm-lib-linux-x64": "2.14.15"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "@earendil-works/pi-ai": "~0.80.10",