@nitra/cursor 12.15.1 → 12.16.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 +16 -0
- package/bin/n-cursor.js +1 -1
- package/lib/docs/index.md +9 -6
- package/lib/docs/pi-agent-fix.md +28 -0
- package/lib/docs/pi-agent-skill.md +36 -0
- package/lib/docs/pi-model-tiers.md +46 -0
- package/lib/docs/pi-one-shot.md +34 -0
- package/lib/docs/pi-telemetry-store.md +33 -0
- package/lib/docs/pi-trace.md +27 -0
- package/lib/docs/pi-write-guard.md +32 -0
- package/lib/pi-agent-fix.mjs +253 -0
- package/lib/pi-agent-skill.mjs +181 -0
- package/lib/pi-model-tiers.mjs +109 -0
- package/lib/pi-one-shot.mjs +129 -0
- package/lib/pi-telemetry-store.mjs +0 -0
- package/lib/pi-trace.mjs +40 -0
- package/lib/pi-write-guard.mjs +147 -0
- package/package.json +5 -1
- package/rules/doc-files/js/docgen-files-batch.mjs +20 -5
- package/rules/doc-files/js/docgen-gen.mjs +42 -25
- package/rules/doc-files/js/docgen-judge-measure.mjs +16 -13
- package/rules/doc-files/js/docgen-judge.mjs +11 -9
- package/rules/doc-files/js/docs/docgen-files-batch.md +3 -20
- package/rules/doc-files/js/docs/docgen-gen.md +3 -20
- package/rules/doc-files/js/docs/docgen-judge-measure.md +3 -18
- package/rules/doc-files/js/docs/docgen-judge.md +3 -22
- package/rules/npm-module/js/docs/skill_meta.md +22 -15
- package/rules/npm-module/js/skill_meta.mjs +5 -1
- package/rules/text/js/cspell-fix.mjs +15 -16
- package/rules/text/js/docs/cspell-fix.md +16 -9
- package/rules/text/main.mjs +4 -4
- package/schemas/skill-meta.json +8 -0
- package/scripts/docs/skills-cli.md +21 -25
- package/scripts/lib/adr/docs/normalize-cli.md +3 -20
- package/scripts/lib/adr/docs/normalize-pipeline.md +3 -33
- package/scripts/lib/adr/normalize-cli.mjs +2 -2
- package/scripts/lib/adr/normalize-pipeline.mjs +78 -44
- package/scripts/lib/docs/skill-meta.md +27 -10
- package/scripts/lib/fix/docs/escalation-log.md +10 -9
- package/scripts/lib/fix/docs/orchestrator.md +13 -20
- package/scripts/lib/fix/escalation-log.mjs +1 -1
- package/scripts/lib/fix/orchestrator.mjs +65 -31
- package/scripts/lib/skill-meta.mjs +22 -0
- package/scripts/skills-cli.mjs +52 -14
- package/scripts/utils/ast-extract.mjs +105 -0
- package/scripts/utils/docs/ast-extract.md +30 -0
- package/lib/docs/llm.md +0 -33
- package/lib/docs/models.md +0 -48
- package/lib/docs/omlx-trace.md +0 -49
- package/lib/docs/omlx.md +0 -41
- package/lib/llm.mjs +0 -215
- package/lib/models.mjs +0 -75
- package/lib/omlx-trace.mjs +0 -158
- package/lib/omlx.mjs +0 -220
- package/scripts/lib/fix/docs/llm-fix-apply.md +0 -31
- package/scripts/lib/fix/docs/llm-lint-fix.md +0 -31
- package/scripts/lib/fix/docs/llm-worker.md +0 -28
- package/scripts/lib/fix/docs/verbose-block.md +0 -27
- package/scripts/lib/fix/llm-fix-apply.mjs +0 -113
- package/scripts/lib/fix/llm-lint-fix.mjs +0 -82
- package/scripts/lib/fix/llm-worker.mjs +0 -346
- package/scripts/lib/fix/verbose-block.mjs +0 -82
package/lib/docs/omlx-trace.md
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: omlx-trace.mjs
|
|
4
|
-
resource: npm/lib/omlx-trace.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: 3ff568d5
|
|
7
|
-
score: 100
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
Модуль збирає трасу викликів LLM, фіксуючи думки моделі та спостережуваний слід. Траса записується у локальний файл для ротації та готується для подальшої батч-агрегації. Дизайн-специфікація: docs/specs/2026-06-10-omlx-wire-trace-capture-design.md.
|
|
11
|
-
|
|
12
|
-
## Поведінка
|
|
13
|
-
|
|
14
|
-
MAX_MSG_CHARS
|
|
15
|
-
Ліміт символів на одне message.content у записі
|
|
16
|
-
|
|
17
|
-
ROTATE_BYTES
|
|
18
|
-
Поріг недеструктивної ротації активного файлу в байтах
|
|
19
|
-
|
|
20
|
-
tracePath
|
|
21
|
-
Повертає шлях до trace-файлу або null
|
|
22
|
-
|
|
23
|
-
capMessages
|
|
24
|
-
Обрізає message.content та рахує sha256 повного масиву
|
|
25
|
-
|
|
26
|
-
buildTraceRecord
|
|
27
|
-
Будує нормалізований trace-запис
|
|
28
|
-
|
|
29
|
-
rotateIfNeeded
|
|
30
|
-
Недеструктивно ротує файл при перевищенні ROTATE_BYTES
|
|
31
|
-
|
|
32
|
-
writeTrace
|
|
33
|
-
Записує trace-запис у файл з ротацією та fail-safe
|
|
34
|
-
|
|
35
|
-
## Публічний API
|
|
36
|
-
|
|
37
|
-
MAX_MSG_CHARS — встановлює максимальну кількість символів для вмісту повідомлення.
|
|
38
|
-
ROTATE_BYTES — визначає поріг для ініціювання ротації файлу.
|
|
39
|
-
tracePath — зберігає шлях до файлу трасування або порожній рядок, якщо трасування вимкнено.
|
|
40
|
-
N_CURSOR_LLM_TRACE — визначає пріоритет для вибору шляху трасування: спочатку перевіряється вимкнений перемикач, потім використовується явний шлях.
|
|
41
|
-
buildTraceRecord — створює запис трасування, залишаючи порожні значення для полів, які недоступні.
|
|
42
|
-
rotateIfNeeded — перейменовує файл при перевищенні ліміту, створюючи новий файл з послідовним індексом.
|
|
43
|
-
writeTrace — записує один запис трасування, обробляючи шляхи, ротуючи дані за необхідності та обробляючи помилки вводу/виводу.
|
|
44
|
-
|
|
45
|
-
## Гарантії поведінки
|
|
46
|
-
|
|
47
|
-
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
48
|
-
- За невдачі повертає значення помилки (`false`/`null`/`Err`) замість генерування винятку чи паніки.
|
|
49
|
-
- Не звертається до мережі.
|
package/lib/docs/omlx.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: omlx.mjs
|
|
4
|
-
resource: npm/lib/omlx.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: 23163fe9
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 100
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
Модуль реалізує логіку маршрутизації запитів до локального omlx-сервера, який надає OpenAI-сумісні відповіді за адресою `http://localhost:8000/v1/chat/completions`. Константа DEFAULT_OMLX_URL визначає цю базову адресу. Модуль визначає, чи є модель локальною, аналізуючи префікс у `model-id` (`omlx/<model>`). Якщо модель локальна, виконується прямий HTTP-запит до omlx. Якщо ж модель не локальна, запит спрямовується до pi CLI. При необхідності, API-ключ для omlx резолвиться з конфігураційного файлу settings.json або змінної N_CURSOR_OMLX_KEY і передається у заголовку `Authorization: Bearer …`. Обробка обмежується лише текстовим контентом, оскільки сервер не підтримує виклики інструментів.
|
|
12
|
-
|
|
13
|
-
## Поведінка
|
|
14
|
-
|
|
15
|
-
DEFAULT_OMLX_URL надає стандартний URL для доступу до omlx-сервера, який є `http://127.0.0.1:8000/v1/chat/completions`.
|
|
16
|
-
resolveOmlxApiKey визначає API-ключ для omlx-сервера, шукаючи його в опціях виклику, змінній середовища `N_CURSOR_OMLX_KEY` або в файлі `~/.omlx/settings.json`.
|
|
17
|
-
isOmlxModel перевіряє, чи відповідає наданий model-id локальному omlx-бекенду, тобто чи починається він з префікса `omlx/`.
|
|
18
|
-
omlxModelId видаляє префікс `omlx/` з model-id, якщо він присутній, для отримання чистого ідентифікатора для omlx API.
|
|
19
|
-
extractReasoning витягує текст думок (reasoning) з відповіді omlx-сервера, надаючи його разом із джерелом вилучення (поле, тег ``або зрізаний контент).
|
|
20
|
-
callOmlxRaw виконує блокуючий HTTP-виклик до omlx-сервера через`curl`, повторюючи спроби при транзієнтних помилках, і повертає багатий об'єкт з контентом, думками, використанням та статусом завершення.
|
|
21
|
-
callOmlx викликає `callOmlxRaw` і повертає лише текстовий контент відповіді від omlx-сервера.
|
|
22
|
-
|
|
23
|
-
## Публічний API
|
|
24
|
-
|
|
25
|
-
DEFAULT_OMLX_URL — Дефолтний URL для звернення до omlx-сервера, який може бути перевизначений.
|
|
26
|
-
|
|
27
|
-
resolveOmlxApiKey — Визначає ключ доступу до omlx-сервера, використовуючи пріоритет: явний ключ, змінна середовища, конфігурація з `settings.json` або відсутність ключа.
|
|
28
|
-
|
|
29
|
-
isOmlxModel — Визначає, чи ідентифікатор моделі вказує на локальний omlx-бекенд (за наявністю префікса `omlx/`).
|
|
30
|
-
|
|
31
|
-
omlxModelId — Видаляє префікс `omlx/` з ідентифікатора моделі, залишаючи чистий ідентифікатор для API.
|
|
32
|
-
|
|
33
|
-
extractReasoning — Витягує внутрішні роздуми моделі з відповіді omlx, шукаючи їх у спеціальному полі, тегу `` або в тексті, якщо відповідь була обрізана.
|
|
34
|
-
|
|
35
|
-
callOmlxRaw — Здійснює прямий HTTP-запит до omlx, повертаючи повний об'єкт відповіді (включаючи контент, роздуми, використання та причину завершення). Автоматично повторює запит при тимчасових збоях.
|
|
36
|
-
|
|
37
|
-
callOmlx — Спрощує виклик до omlx, повертаючи лише текстовий вміст відповіді, ігноруючи інші деталі.
|
|
38
|
-
|
|
39
|
-
## Гарантії поведінки
|
|
40
|
-
|
|
41
|
-
- Read-only: не виконує операцій запису (ФС/БД).
|
package/lib/llm.mjs
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Єдина точка LLM-викликів для JS-оркестраторів (див. ADR 260610-2228).
|
|
3
|
-
*
|
|
4
|
-
* Маршрутизація — виключно за префіксом model-id (конвенція `npm/lib/models.mjs`):
|
|
5
|
-
* `omlx/<model>` → прямий HTTP до локального omlx-сервера (`callOmlx`)
|
|
6
|
-
* будь-що інше → `pi` CLI (хмарні провайдери або pi-дефолт)
|
|
7
|
-
*
|
|
8
|
-
* Жодних env-перемикачів бекенда: рядок моделі сам визначає транспорт.
|
|
9
|
-
*
|
|
10
|
-
* Wire-trace (спека 2026-06-10-omlx-wire-trace-capture-design): **always-on**
|
|
11
|
-
* багатий JSONL-запис на кожен виклик — обидва канали (reasoning + слід). Для
|
|
12
|
-
* omlx захоплює content/reasoning/usage/finish_reason/attempts; для pi — лише
|
|
13
|
-
* те, що CLI дає (rich-поля null). Деталі запису/шляху/ротації — `omlx-trace.mjs`.
|
|
14
|
-
*/
|
|
15
|
-
import { spawnSync } from 'node:child_process'
|
|
16
|
-
import { env } from 'node:process'
|
|
17
|
-
|
|
18
|
-
import { callOmlxRaw, isOmlxModel } from './omlx.mjs'
|
|
19
|
-
import { buildTraceRecord, writeTrace } from './omlx-trace.mjs'
|
|
20
|
-
|
|
21
|
-
/** Дефолтний timeout одного виклику (узгоджено з LOCAL_TIMEOUT доки-конвеєра). */
|
|
22
|
-
const DEFAULT_TIMEOUT_MS = 120_000
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Бекенд для model-id: `omlx` — прямий HTTP, `pi` — CLI.
|
|
26
|
-
* @param {string} model model-id (можливо порожній — pi-дефолт)
|
|
27
|
-
* @returns {'omlx'|'pi'} назва бекенда
|
|
28
|
-
*/
|
|
29
|
-
export function pickBackend(model) {
|
|
30
|
-
return isOmlxModel(model) ? 'omlx' : 'pi'
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Виклик через `pi` CLI: messages конкатенуються у plain prompt
|
|
35
|
-
* (pi не приймає messages-масив), tools вимкнено.
|
|
36
|
-
* @param {Array<{role:string, content:string}>} messages OpenAI-style messages
|
|
37
|
-
* @param {string} model model-id для `--model` (порожній — pi-дефолт)
|
|
38
|
-
* @param {number} timeoutMs ліміт очікування процесу
|
|
39
|
-
* @returns {string} stdout відповіді
|
|
40
|
-
*/
|
|
41
|
-
function callPi(messages, model, timeoutMs) {
|
|
42
|
-
const prompt = messages.map(m => m.content).join('\n\n')
|
|
43
|
-
const modelArgs = model ? ['--model', model] : []
|
|
44
|
-
const r = spawnSync('pi', ['-p', prompt, ...modelArgs, '--no-session', '--mode', 'text', '--no-tools'], {
|
|
45
|
-
encoding: 'utf8',
|
|
46
|
-
timeout: timeoutMs
|
|
47
|
-
})
|
|
48
|
-
if (r.error) throw new Error(`pi error: ${r.error.message}`)
|
|
49
|
-
if (r.status !== 0) throw new Error(`pi exit ${r.status}: ${r.stderr?.slice(0, 300) ?? ''}`)
|
|
50
|
-
return r.stdout?.trim() ?? ''
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Універсальний LLM-виклик з маршрутизацією за префіксом model-id і always-on
|
|
55
|
-
* wire-trace (обидва канали). Повертає **багатий** обʼєкт із вмістом і reasoning.
|
|
56
|
-
* @param {Array<{role:string, content:string}>} messages OpenAI-style messages (system зберігається на omlx)
|
|
57
|
-
* @param {string} model model-id; `omlx/<m>` → прямий HTTP, інакше → pi CLI
|
|
58
|
-
* @param {{ timeoutMs?: number, temperature?: number, maxTokens?: number, url?: string, caller?: string, thinkingBudget?: number }} [opts] timeout, температура, ліміт виходу, override URL, мітка викликача для trace, бюджет thinking-токенів (лише omlx)
|
|
59
|
-
* @returns {{ content: string, reasoning: string|null, reasoningSource: string|null }} вміст відповіді і thinking-монолог
|
|
60
|
-
*/
|
|
61
|
-
export function callLlmRich(messages, model, opts = {}) {
|
|
62
|
-
const { timeoutMs = DEFAULT_TIMEOUT_MS, temperature = 0.2, maxTokens, url, caller, thinkingBudget } = opts
|
|
63
|
-
const backend = pickBackend(model)
|
|
64
|
-
const resolvedCaller = caller ?? env.N_CURSOR_TRACE_CALLER ?? 'unknown'
|
|
65
|
-
const t0 = Date.now()
|
|
66
|
-
try {
|
|
67
|
-
let content
|
|
68
|
-
let reasoning = null
|
|
69
|
-
let reasoningSource = null
|
|
70
|
-
let finishReason = null
|
|
71
|
-
let usage = null
|
|
72
|
-
let attempts = 1
|
|
73
|
-
if (backend === 'omlx') {
|
|
74
|
-
const raw = callOmlxRaw(messages, model, {
|
|
75
|
-
url,
|
|
76
|
-
timeoutMs,
|
|
77
|
-
temperature,
|
|
78
|
-
...(maxTokens ? { maxTokens } : {}),
|
|
79
|
-
...(thinkingBudget ? { thinkingBudget } : {})
|
|
80
|
-
})
|
|
81
|
-
;({ content, reasoning, reasoningSource, finishReason, usage, attempts } = raw)
|
|
82
|
-
} else {
|
|
83
|
-
content = callPi(messages, model, timeoutMs)
|
|
84
|
-
}
|
|
85
|
-
writeTrace(
|
|
86
|
-
buildTraceRecord({
|
|
87
|
-
ts: new Date().toISOString(),
|
|
88
|
-
caller: resolvedCaller,
|
|
89
|
-
backend,
|
|
90
|
-
model,
|
|
91
|
-
temperature,
|
|
92
|
-
maxTokens,
|
|
93
|
-
messages,
|
|
94
|
-
content,
|
|
95
|
-
reasoning,
|
|
96
|
-
reasoningSource,
|
|
97
|
-
finishReason,
|
|
98
|
-
usage,
|
|
99
|
-
ms: Date.now() - t0,
|
|
100
|
-
attempts,
|
|
101
|
-
ok: true,
|
|
102
|
-
error: null
|
|
103
|
-
})
|
|
104
|
-
)
|
|
105
|
-
return { content, reasoning, reasoningSource }
|
|
106
|
-
} catch (error) {
|
|
107
|
-
writeTrace(
|
|
108
|
-
buildTraceRecord({
|
|
109
|
-
ts: new Date().toISOString(),
|
|
110
|
-
caller: resolvedCaller,
|
|
111
|
-
backend,
|
|
112
|
-
model,
|
|
113
|
-
temperature,
|
|
114
|
-
maxTokens,
|
|
115
|
-
messages,
|
|
116
|
-
ms: Date.now() - t0,
|
|
117
|
-
attempts: null,
|
|
118
|
-
ok: false,
|
|
119
|
-
error: String(error.message).slice(0, 200)
|
|
120
|
-
})
|
|
121
|
-
)
|
|
122
|
-
throw error
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Тонка обгортка над `callLlmRich` — повертає лише рядок контенту. Зберігає
|
|
128
|
-
* backward-compatible контракт для споживачів, яким reasoning не потрібен.
|
|
129
|
-
* @param {Array<{role:string, content:string}>} messages OpenAI-style messages
|
|
130
|
-
* @param {string} model model-id
|
|
131
|
-
* @param {{ timeoutMs?: number, temperature?: number, maxTokens?: number, url?: string, caller?: string }} [opts]
|
|
132
|
-
* @returns {string} текст відповіді
|
|
133
|
-
*/
|
|
134
|
-
export function callLlm(messages, model, opts = {}) {
|
|
135
|
-
return callLlmRich(messages, model, opts).content
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Фрагмент повідомлення omlx про memory-guard (динамічна стеля пам'яті). */
|
|
139
|
-
const MEMORY_GUARD_MARKER = 'memory ceiling'
|
|
140
|
-
/** Тип помилки omlx про відсутній/хибний API-ключ. */
|
|
141
|
-
const AUTH_ERROR_MARKER = 'authentication_error'
|
|
142
|
-
/** Детерміновані помилки: контекст/модель — ретрай чи чекання не допоможе. */
|
|
143
|
-
const PERMANENT_RE = /too long|exceeds[^.]*context|not found/i
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Класифікує omlx-помилку **після** того, як `callOmlxRaw` вичерпав внутрішні
|
|
147
|
-
* ретраї — для реакції оркестратора (skip vs circuit-breaker vs звичайна помилка):
|
|
148
|
-
* - `permanent` — детерміновано (контекст завеликий, модель відсутня): skip, не ретраїти;
|
|
149
|
-
* - `systemic` — середовище/сервер (memory-guard, auth, down/таймаут): каскадить → circuit-breaker;
|
|
150
|
-
* - `transient` — решта (empty content, bad json): рідкісне, не каскадить.
|
|
151
|
-
* @param {string} message текст `error.message`
|
|
152
|
-
* @returns {'transient'|'systemic'|'permanent'} клас помилки
|
|
153
|
-
*/
|
|
154
|
-
export function classifyOmlxError(message) {
|
|
155
|
-
const m = String(message)
|
|
156
|
-
if (PERMANENT_RE.test(m)) return 'permanent'
|
|
157
|
-
if (m.includes(MEMORY_GUARD_MARKER) || m.includes(AUTH_ERROR_MARKER) || m.startsWith('omlx curl')) {
|
|
158
|
-
return 'systemic'
|
|
159
|
-
}
|
|
160
|
-
return 'transient'
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Preflight-перевірка omlx перед масовим прогоном: мінімальний chat-виклик
|
|
165
|
-
* (`max_tokens: 1`). Розрізняє стани, які вимагають різних дій:
|
|
166
|
-
* - `down` — сервер не відповідає (не запущений / не той порт);
|
|
167
|
-
* - `memory-guard` — модель не влазить у динамічну стелю пам'яті зайнятої
|
|
168
|
-
* машини → «відклади прогін», а не «модель погана»;
|
|
169
|
-
* - `auth` — сервер вимагає API-ключ → вистав `N_CURSOR_OMLX_KEY`;
|
|
170
|
-
* - `error` — інша помилка API.
|
|
171
|
-
* Порожній контент відповіді — це ok: сервер живий і модель завантажена.
|
|
172
|
-
* @param {{ url?: string, model?: string, timeoutMs?: number }} [opts] override URL/моделі/timeout перевірки
|
|
173
|
-
* @returns {{ ok: boolean, reason: 'down'|'memory-guard'|'auth'|'error'|null, detail: string }} стан сервера і класифікована причина збою
|
|
174
|
-
*/
|
|
175
|
-
export function omlxHealthCheck(opts = {}) {
|
|
176
|
-
const { url, model = '', timeoutMs = DEFAULT_TIMEOUT_MS } = opts
|
|
177
|
-
try {
|
|
178
|
-
callOmlxRaw([{ role: 'user', content: 'ok' }], model, { url, timeoutMs, maxTokens: 1, temperature: 0 })
|
|
179
|
-
return { ok: true, reason: null, detail: '' }
|
|
180
|
-
} catch (error) {
|
|
181
|
-
const detail = String(error.message)
|
|
182
|
-
if (detail.includes(MEMORY_GUARD_MARKER)) return { ok: false, reason: 'memory-guard', detail }
|
|
183
|
-
if (detail.includes(AUTH_ERROR_MARKER)) return { ok: false, reason: 'auth', detail }
|
|
184
|
-
if (detail.startsWith('omlx empty content')) return { ok: true, reason: null, detail }
|
|
185
|
-
if (detail.startsWith('omlx curl')) return { ok: false, reason: 'down', detail }
|
|
186
|
-
return { ok: false, reason: 'error', detail }
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Спільний preflight локальної fix/gen-моделі (opportunistic LLM-fix tier, спека
|
|
192
|
-
* docs/specs/2026-06-15-opportunistic-llm-fix-tier.md): чи можна звати модель зараз.
|
|
193
|
-
* Health-check лише для omlx-бекенду (pi/cloud — завжди дозволено). Використовують
|
|
194
|
-
* doc-files (генерація) і text/cspell (класифікація) — fast-skip замість приречених викликів.
|
|
195
|
-
* @param {string} model model-id (зазвичай `N_LOCAL_MIN_MODEL`)
|
|
196
|
-
* @returns {string|null} людинозрозумілий текст проблеми, або null якщо можна викликати
|
|
197
|
-
*/
|
|
198
|
-
export function preflightLocalModel(model) {
|
|
199
|
-
if (!model) {
|
|
200
|
-
return 'модель не задано. Вистав N_LOCAL_MIN_MODEL (напр. omlx/mlx-community--gemma-4-e4b-it-OptiQ-4bit) і повтори.'
|
|
201
|
-
}
|
|
202
|
-
if (pickBackend(model) !== 'omlx') return null
|
|
203
|
-
const hc = omlxHealthCheck({ model })
|
|
204
|
-
if (hc.ok) return null
|
|
205
|
-
if (hc.reason === 'memory-guard') {
|
|
206
|
-
return `omlx memory-guard: модель не влазить у динамічну стелю пам'яті (машина зайнята).\n Звільни пам'ять або повтори прогін пізніше.\n ${hc.detail}`
|
|
207
|
-
}
|
|
208
|
-
if (hc.reason === 'down') {
|
|
209
|
-
return `omlx-сервер не відповідає. Запусти \`omlx serve\` і повтори.\n ${hc.detail}`
|
|
210
|
-
}
|
|
211
|
-
if (hc.reason === 'auth') {
|
|
212
|
-
return `omlx вимагає API-ключ. Вистав N_CURSOR_OMLX_KEY (auth.api_key з ~/.omlx/settings.json).\n ${hc.detail}`
|
|
213
|
-
}
|
|
214
|
-
return `omlx помилка: ${hc.detail}`
|
|
215
|
-
}
|
package/lib/models.mjs
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Глобальна класифікація моделей для pi.
|
|
3
|
-
*
|
|
4
|
-
* Формат значень: "provider/model-id" (pi --model формат).
|
|
5
|
-
* Налаштовується один раз у середовищі; кожен скіл посилається на потрібний тир.
|
|
6
|
-
*
|
|
7
|
-
* Приклад ~/.bashrc або .env:
|
|
8
|
-
* N_LOCAL_MIN_MODEL=omlx/mlx-community--gemma-4-e2b-it-4bit
|
|
9
|
-
* N_CLOUD_MIN_MODEL=openai/gpt-5.4-mini
|
|
10
|
-
* N_CLOUD_AVG_MODEL=openai/gpt-5.4
|
|
11
|
-
* N_CLOUD_MAX_MODEL=openai/gpt-5.5
|
|
12
|
-
*
|
|
13
|
-
* Значення '' означає "pi дефолтний провайдер" (залежить від ~/.pi конфігу).
|
|
14
|
-
*
|
|
15
|
-
* ## Бекенд за префіксом model-id
|
|
16
|
-
*
|
|
17
|
-
* model-id з префіксом `omlx/...` іде прямим HTTP до локального
|
|
18
|
-
* omlx-сервера (`npm/lib/omlx.mjs`), минаючи pi; решта (`openai/...`,
|
|
19
|
-
* `ollama/...`, '') — через pi CLI. Тому локальні тири варто задавати у форматі
|
|
20
|
-
* `omlx/<model>`, аби local-inference йшов напряму, а pi лишався шаром для хмари
|
|
21
|
-
* (див. ADR 260610-1349).
|
|
22
|
-
*
|
|
23
|
-
* ## Каскад local → cloud (контракт)
|
|
24
|
-
*
|
|
25
|
-
* Використовуйте resolveModel(tier) замість прямих констант — система прозоро
|
|
26
|
-
* відпрацює навіть без локальних моделей:
|
|
27
|
-
*
|
|
28
|
-
* resolveModel('min') → LOCAL_MIN → LOCAL_AVG → LOCAL_MAX → CLOUD_MIN
|
|
29
|
-
* resolveModel('avg') → LOCAL_AVG → LOCAL_MAX → CLOUD_AVG
|
|
30
|
-
* resolveModel('max') → LOCAL_MAX → CLOUD_MAX
|
|
31
|
-
*
|
|
32
|
-
* Якщо жоден тир не задано — повертає '' (pi-дефолт провайдера).
|
|
33
|
-
* Прямі константи (LOCAL_MIN тощо) залишені для випадків, де потрібен
|
|
34
|
-
* явний контроль (напр., ollama HTTP, explicit retry до хмари).
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
import { env } from 'node:process'
|
|
38
|
-
|
|
39
|
-
// ── Локальні (offline, без API-ключа) ────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
/** Швидкий локальний inference. Напр.: ollama/gemma3:4b */
|
|
42
|
-
export const LOCAL_MIN = env.N_LOCAL_MIN_MODEL ?? ''
|
|
43
|
-
|
|
44
|
-
/** Середній локальний. Напр.: ollama/gemma4:26b-moe */
|
|
45
|
-
export const LOCAL_AVG = env.N_LOCAL_AVG_MODEL ?? ''
|
|
46
|
-
|
|
47
|
-
/** Максимальний локальний. Напр.: ollama/llama4-maverick */
|
|
48
|
-
export const LOCAL_MAX = env.N_LOCAL_MAX_MODEL ?? ''
|
|
49
|
-
|
|
50
|
-
// ── Хмарні (потрібен API-ключ у pi) ─────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
/** Мінімальний хмарний. Напр.: openai/gpt-5.4-mini, google/gemini-2.5-flash, anthropic/claude-haiku-4-5 */
|
|
53
|
-
export const CLOUD_MIN = env.N_CLOUD_MIN_MODEL ?? ''
|
|
54
|
-
|
|
55
|
-
/** Середній хмарний. Напр.: openai/gpt-5.4, google/gemini-2.5-pro, anthropic/claude-sonnet-4-6 */
|
|
56
|
-
export const CLOUD_AVG = env.N_CLOUD_AVG_MODEL ?? ''
|
|
57
|
-
|
|
58
|
-
/** Максимальний хмарний. Напр.: openai/gpt-5.5, anthropic/claude-opus-4-8 */
|
|
59
|
-
export const CLOUD_MAX = env.N_CLOUD_MAX_MODEL ?? ''
|
|
60
|
-
|
|
61
|
-
// ── Каскадне розв'язання ─────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Повертає перший непорожній model-id для запитаного тиру,
|
|
65
|
-
* каскадно перевіряючи локальні тири, а тоді хмарний еквівалент.
|
|
66
|
-
* @param {'min'|'avg'|'max'} tier тир запитуваної моделі
|
|
67
|
-
* @returns {string} provider/model-id або '' для pi-дефолту
|
|
68
|
-
* @throws {TypeError} якщо tier невідомий
|
|
69
|
-
*/
|
|
70
|
-
export function resolveModel(tier) {
|
|
71
|
-
if (tier === 'min') return LOCAL_MIN || LOCAL_AVG || LOCAL_MAX || CLOUD_MIN
|
|
72
|
-
if (tier === 'avg') return LOCAL_AVG || LOCAL_MAX || CLOUD_AVG
|
|
73
|
-
if (tier === 'max') return LOCAL_MAX || CLOUD_MAX
|
|
74
|
-
throw new TypeError(`resolveModel: unknown tier "${tier}". Use 'min', 'avg', or 'max'.`)
|
|
75
|
-
}
|
package/lib/omlx-trace.mjs
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wire-trace LLM-викликів: будує й пише багатий JSONL-запис на кожен виклик
|
|
3
|
-
* `callLlm` (див. `npm/lib/llm.mjs`). Захоплює **обидва канали** — reasoning
|
|
4
|
-
* (думки моделі) і спостережуваний слід (request/response/usage/latency/retry).
|
|
5
|
-
*
|
|
6
|
-
* Дизайн-спека: `docs/specs/2026-06-10-omlx-wire-trace-capture-design.md`.
|
|
7
|
-
*
|
|
8
|
-
* Двошарова модель:
|
|
9
|
-
* - RAW (цей модуль) → `<cwd>/.n-cursor/llm-trace.jsonl` (gitignored, локальний,
|
|
10
|
-
* недеструктивна ротація) — сирий потік, доживає до батч-агрегації.
|
|
11
|
-
* - AGGREGATE (друга спека) → `docs/omlx-insights/` (коммітиться в git, назавжди).
|
|
12
|
-
*
|
|
13
|
-
* Always-on: пишеться завжди. `N_CURSOR_LLM_TRACE=0|false|off|no` — kill-switch;
|
|
14
|
-
* будь-яке інше значення — override-шлях замість дефолтного.
|
|
15
|
-
*/
|
|
16
|
-
import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from 'node:fs'
|
|
17
|
-
import { createHash } from 'node:crypto'
|
|
18
|
-
import { dirname, join } from 'node:path'
|
|
19
|
-
import { cwd, env } from 'node:process'
|
|
20
|
-
|
|
21
|
-
/** Ліміт символів на одне `message.content` у записі (захист обсягу/чутливості). */
|
|
22
|
-
export const MAX_MSG_CHARS = 8000
|
|
23
|
-
|
|
24
|
-
/** Поріг недеструктивної ротації активного файлу (байти). */
|
|
25
|
-
export const ROTATE_BYTES = 50 * 1024 * 1024
|
|
26
|
-
|
|
27
|
-
/** Значення `N_CURSOR_LLM_TRACE`, що вимикають трасування повністю. */
|
|
28
|
-
const KILL_VALUES = new Set(['0', 'false', 'off', 'no'])
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Шлях активного trace-файлу або `null`, якщо трасування вимкнено kill-switch-ем.
|
|
32
|
-
* Пріоритет: `N_CURSOR_LLM_TRACE` (kill-switch → null; інакше явний шлях) →
|
|
33
|
-
* дефолт `<cwd>/.n-cursor/llm-trace.jsonl` (корінь споживацького проєкту).
|
|
34
|
-
* @returns {string|null} абсолютний/відносний шлях до .jsonl або null
|
|
35
|
-
*/
|
|
36
|
-
export function tracePath() {
|
|
37
|
-
const override = env.N_CURSOR_LLM_TRACE
|
|
38
|
-
if (override !== undefined) {
|
|
39
|
-
if (KILL_VALUES.has(override.toLowerCase())) return null
|
|
40
|
-
if (override) return override
|
|
41
|
-
}
|
|
42
|
-
return join(cwd(), '.n-cursor', 'llm-trace.jsonl')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Обрізає кожне `message.content` до `MAX_MSG_CHARS` і рахує sha256 повного
|
|
47
|
-
* (необрізаного) масиву для дедуплікації.
|
|
48
|
-
* @param {Array<{role:string, content:string}>} messages вихідні messages
|
|
49
|
-
* @returns {{ messages: Array<{role:string, content:string}>, messages_sha256: string, messages_truncated: boolean }} обрізані messages, hash і прапор обрізки
|
|
50
|
-
*/
|
|
51
|
-
export function capMessages(messages) {
|
|
52
|
-
const src = messages ?? []
|
|
53
|
-
let truncated = false
|
|
54
|
-
const capped = src.map(m => {
|
|
55
|
-
const content = m?.content ?? ''
|
|
56
|
-
if (content.length > MAX_MSG_CHARS) {
|
|
57
|
-
truncated = true
|
|
58
|
-
return { role: m.role, content: content.slice(0, MAX_MSG_CHARS) }
|
|
59
|
-
}
|
|
60
|
-
return { role: m?.role, content }
|
|
61
|
-
})
|
|
62
|
-
const messages_sha256 = createHash('sha256').update(JSON.stringify(src)).digest('hex')
|
|
63
|
-
return { messages: capped, messages_sha256, messages_truncated: truncated }
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Будує нормалізований trace-запис. Поля, яких backend не дає (pi: reasoning/
|
|
68
|
-
* usage/finish_reason), лишаються `null` за побудовою.
|
|
69
|
-
* @param {object} i вхід
|
|
70
|
-
* @param {string} i.ts ISO-час завершення виклику
|
|
71
|
-
* @param {string} i.caller хто викликав (doc-files|fix|coverage|unknown)
|
|
72
|
-
* @param {'omlx'|'pi'} i.backend бекенд
|
|
73
|
-
* @param {string} i.model model-id
|
|
74
|
-
* @param {number} [i.temperature] температура
|
|
75
|
-
* @param {number} [i.maxTokens] ліміт виходу
|
|
76
|
-
* @param {Array<{role:string, content:string}>} i.messages messages запиту
|
|
77
|
-
* @param {string|null} [i.content] відповідь
|
|
78
|
-
* @param {string|null} [i.reasoning] думки моделі
|
|
79
|
-
* @param {string|null} [i.reasoningSource] джерело reasoning
|
|
80
|
-
* @param {string|null} [i.finishReason] finish_reason
|
|
81
|
-
* @param {object|null} [i.usage] usage verbatim
|
|
82
|
-
* @param {number} i.ms latency
|
|
83
|
-
* @param {number|null} [i.attempts] кількість спроб
|
|
84
|
-
* @param {boolean} i.ok успіх
|
|
85
|
-
* @param {string|null} [i.error] текст помилки
|
|
86
|
-
* @returns {object} JSONL-готовий запис
|
|
87
|
-
*/
|
|
88
|
-
export function buildTraceRecord(i) {
|
|
89
|
-
const capped = capMessages(i.messages)
|
|
90
|
-
return {
|
|
91
|
-
ts: i.ts,
|
|
92
|
-
caller: i.caller,
|
|
93
|
-
backend: i.backend,
|
|
94
|
-
model: i.model,
|
|
95
|
-
temperature: i.temperature ?? null,
|
|
96
|
-
max_tokens: i.maxTokens ?? null,
|
|
97
|
-
messages: capped.messages,
|
|
98
|
-
messages_sha256: capped.messages_sha256,
|
|
99
|
-
messages_truncated: capped.messages_truncated,
|
|
100
|
-
content: i.content ?? null,
|
|
101
|
-
reasoning: i.reasoning ?? null,
|
|
102
|
-
reasoning_source: i.reasoningSource ?? null,
|
|
103
|
-
finish_reason: i.finishReason ?? null,
|
|
104
|
-
usage: i.usage ?? null,
|
|
105
|
-
ms: i.ms,
|
|
106
|
-
attempts: i.attempts ?? null,
|
|
107
|
-
ok: i.ok,
|
|
108
|
-
error: i.error ?? null
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Імʼя архіву для ротації: `llm-trace.jsonl` → `llm-trace.<seq>.jsonl`
|
|
114
|
-
* (нестандартні імена без `.jsonl` → `<file>.<seq>`).
|
|
115
|
-
* @param {string} file активний trace-файл
|
|
116
|
-
* @param {number} seq порядковий номер архіву
|
|
117
|
-
* @returns {string} шлях архіву
|
|
118
|
-
*/
|
|
119
|
-
function archiveName(file, seq) {
|
|
120
|
-
return file.endsWith('.jsonl') ? `${file.slice(0, -'.jsonl'.length)}.${seq}.jsonl` : `${file}.${seq}`
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Недеструктивна ротація: якщо активний файл перевищує `ROTATE_BYTES`,
|
|
125
|
-
* перейменовує його в перший вільний `llm-trace.<seq>.jsonl` (без перезапису
|
|
126
|
-
* наявних архівів). Відсутній файл / помилка stat — no-op.
|
|
127
|
-
* @param {string} file активний trace-файл
|
|
128
|
-
*/
|
|
129
|
-
export function rotateIfNeeded(file) {
|
|
130
|
-
let size
|
|
131
|
-
try {
|
|
132
|
-
size = statSync(file).size
|
|
133
|
-
} catch {
|
|
134
|
-
return // файлу ще нема — нічого ротувати
|
|
135
|
-
}
|
|
136
|
-
if (size <= ROTATE_BYTES) return
|
|
137
|
-
let seq = 1
|
|
138
|
-
while (existsSync(archiveName(file, seq))) seq++
|
|
139
|
-
renameSync(file, archiveName(file, seq))
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Fail-safe запис одного trace-рядка. Резолвить шлях (kill-switch → no-op),
|
|
144
|
-
* ротує за потреби, створює теку, append-ить JSONL. Будь-яка помилка IO
|
|
145
|
-
* ковтається — трасування **ніколи** не ламає основний виклик.
|
|
146
|
-
* @param {object} record запис від `buildTraceRecord`
|
|
147
|
-
*/
|
|
148
|
-
export function writeTrace(record) {
|
|
149
|
-
const file = tracePath()
|
|
150
|
-
if (!file) return
|
|
151
|
-
try {
|
|
152
|
-
rotateIfNeeded(file)
|
|
153
|
-
mkdirSync(dirname(file), { recursive: true })
|
|
154
|
-
appendFileSync(file, JSON.stringify(record) + '\n')
|
|
155
|
-
} catch {
|
|
156
|
-
// трейс не має ламати основний виклик
|
|
157
|
-
}
|
|
158
|
-
}
|