@nitra/cursor 5.0.3 → 5.1.0
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/models.mjs +9 -1
- package/lib/omlx.mjs +102 -0
- package/package.json +1 -1
- package/scripts/coverage-classify/index.mjs +24 -15
- package/skills/docgen/js/docgen-compare-pi-vs-direct.mjs +1 -1
- package/skills/docgen/js/docgen-extract-anchors.mjs +4 -4
- package/skills/docgen/js/docgen-gen.mjs +8 -41
- package/skills/fix/js/llm-worker.mjs +20 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [5.1.0] - 2026-06-10
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- local-inference: маршрут моделей за префіксом `omlx/` напряму в omlx HTTP (npm/lib/omlx.mjs) минаючи pi; coverage-classify і fix/llm-worker переведено на спільний callOmlx, docgen де-дубльовано; pi лишається шаром для хмари й агентних задач (ADR 260610-1349)
|
|
8
|
+
|
|
3
9
|
## [5.0.3] - 2026-06-10
|
|
4
10
|
|
|
5
11
|
### Changed
|
package/lib/models.mjs
CHANGED
|
@@ -5,13 +5,21 @@
|
|
|
5
5
|
* Налаштовується один раз у середовищі; кожен скіл посилається на потрібний тир.
|
|
6
6
|
*
|
|
7
7
|
* Приклад ~/.bashrc або .env:
|
|
8
|
-
* N_LOCAL_MIN_MODEL=
|
|
8
|
+
* N_LOCAL_MIN_MODEL=omlx/mlx-community--gemma-4-e2b-it-4bit
|
|
9
9
|
* N_CLOUD_MIN_MODEL=openai/gpt-5.4-mini
|
|
10
10
|
* N_CLOUD_AVG_MODEL=openai/gpt-5.4
|
|
11
11
|
* N_CLOUD_MAX_MODEL=openai/gpt-5.5
|
|
12
12
|
*
|
|
13
13
|
* Значення '' означає "pi дефолтний провайдер" (залежить від ~/.pi конфігу).
|
|
14
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
|
+
*
|
|
15
23
|
* ## Каскад local → cloud (контракт)
|
|
16
24
|
*
|
|
17
25
|
* Використовуйте resolveModel(tier) замість прямих констант — система прозоро
|
package/lib/omlx.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Спільний транспорт до локального omlx-сервера (OpenAI-сумісний MLX,
|
|
3
|
+
* `http://localhost:8000/v1/chat/completions`). Text-only: жодних `tools`/
|
|
4
|
+
* `tool_calls` — сервер їх не підтримує (див. ADR
|
|
5
|
+
* `260610-1349-агентна-пастка-js-owned-loop-через-omlx-замість-pi-tool-loop`).
|
|
6
|
+
*
|
|
7
|
+
* Маршрутизація між omlx і pi — за конвенцією префікса в model-id:
|
|
8
|
+
* `omlx/<model>` → прямий HTTP до omlx (локальний inference, без pi)
|
|
9
|
+
* будь-що інше → pi CLI (хмарні провайдери або pi-дефолт)
|
|
10
|
+
*
|
|
11
|
+
* Так `resolveModel(tier)` лишається незмінним: достатньо виставити локальний
|
|
12
|
+
* тир у форматі `N_LOCAL_MIN_MODEL=omlx/mlx-community--gemma-4-e2b-it-4bit`, і
|
|
13
|
+
* виклик сам піде напряму в omlx замість pi.
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from 'node:child_process'
|
|
16
|
+
import { env } from 'node:process'
|
|
17
|
+
|
|
18
|
+
/** Дефолтний endpoint omlx (override — `N_CURSOR_OMLX_URL`). */
|
|
19
|
+
export const DEFAULT_OMLX_URL = 'http://127.0.0.1:8000/v1/chat/completions'
|
|
20
|
+
|
|
21
|
+
/** Дефолтна модель, якщо в id лишився голий `omlx/` (override — `N_CURSOR_OMLX_MODEL`). */
|
|
22
|
+
export const DEFAULT_OMLX_MODEL = 'mlx-community--gemma-4-e2b-it-4bit'
|
|
23
|
+
|
|
24
|
+
const OMLX_PREFIX = 'omlx/'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Чи цей model-id адресує локальний omlx-бекенд (префікс `omlx/`).
|
|
28
|
+
* @param {unknown} model перевірюваний model-id
|
|
29
|
+
* @returns {boolean} true, якщо рядок починається з `omlx/`
|
|
30
|
+
*/
|
|
31
|
+
export function isOmlxModel(model) {
|
|
32
|
+
return typeof model === 'string' && model.startsWith(OMLX_PREFIX)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Прибирає `omlx/`-префікс → чистий model-id для omlx API.
|
|
37
|
+
* Не-omlx-рядки повертає без змін.
|
|
38
|
+
* @param {string} model model-id (можливо з префіксом)
|
|
39
|
+
* @returns {string} model-id без `omlx/`
|
|
40
|
+
*/
|
|
41
|
+
export function omlxModelId(model) {
|
|
42
|
+
return isOmlxModel(model) ? model.slice(OMLX_PREFIX.length) : model
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Прямий HTTP-виклик до omlx через `curl` (spawnSync). Повертає текст
|
|
47
|
+
* `choices[0].message.content`. Ретраїть лише transient curl-помилки
|
|
48
|
+
* (18 = transfer closed, 52 = empty reply, 56 = recv failure).
|
|
49
|
+
*
|
|
50
|
+
* @param {Array<{role:string, content:string}>} messages OpenAI-messages (system+user збережено)
|
|
51
|
+
* @param {string} model model-id (з/без `omlx/`-префікса); порожній → дефолт
|
|
52
|
+
* @param {{ url?: string, timeoutMs?: number, temperature?: number, maxTokens?: number, fallbackModel?: string }} [opts]
|
|
53
|
+
* @returns {string} непорожній контент відповіді
|
|
54
|
+
* @throws на curl-помилці, не-200 exit, поганому JSON чи порожньому контенті
|
|
55
|
+
*/
|
|
56
|
+
export function callOmlx(messages, model, opts = {}) {
|
|
57
|
+
const {
|
|
58
|
+
url = env.N_CURSOR_OMLX_URL ?? DEFAULT_OMLX_URL,
|
|
59
|
+
timeoutMs = 60_000,
|
|
60
|
+
temperature = 0.2,
|
|
61
|
+
maxTokens = 4096,
|
|
62
|
+
fallbackModel = env.N_CURSOR_OMLX_MODEL ?? DEFAULT_OMLX_MODEL
|
|
63
|
+
} = opts
|
|
64
|
+
|
|
65
|
+
const m = omlxModelId(model) || fallbackModel
|
|
66
|
+
const body = JSON.stringify({ model: m, messages, max_tokens: maxTokens, temperature })
|
|
67
|
+
|
|
68
|
+
const TRANSIENT_CURL_CODES = new Set([18, 52, 56])
|
|
69
|
+
let lastErr
|
|
70
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
71
|
+
const r = spawnSync(
|
|
72
|
+
'curl',
|
|
73
|
+
['-sS', '-X', 'POST', url, '-H', 'Content-Type: application/json', '-H', 'Connection: close', '--max-time', String(Math.ceil(timeoutMs / 1000)), '--data-binary', '@-'],
|
|
74
|
+
{ input: body, encoding: 'utf8', timeout: timeoutMs + 5000 }
|
|
75
|
+
)
|
|
76
|
+
if (r.error) {
|
|
77
|
+
lastErr = new Error(`omlx curl error: ${r.error.message}`)
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
if (r.status !== 0) {
|
|
81
|
+
if (TRANSIENT_CURL_CODES.has(r.status) && attempt < 3) {
|
|
82
|
+
lastErr = new Error(`omlx curl exit ${r.status} (transient, retry ${attempt})`)
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`omlx curl exit ${r.status}: ${r.stderr?.slice(0, 300) ?? ''}`)
|
|
86
|
+
}
|
|
87
|
+
let j
|
|
88
|
+
try {
|
|
89
|
+
j = JSON.parse(r.stdout)
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error(`omlx bad json: ${r.stdout?.slice(0, 200) ?? ''}`)
|
|
92
|
+
}
|
|
93
|
+
if (j.error) throw new Error(`omlx api: ${JSON.stringify(j.error).slice(0, 300)}`)
|
|
94
|
+
const content = j.choices?.[0]?.message?.content?.trim() ?? ''
|
|
95
|
+
if (!content) {
|
|
96
|
+
const finish = j.choices?.[0]?.finish_reason
|
|
97
|
+
throw new Error(`omlx empty content (finish=${finish})`)
|
|
98
|
+
}
|
|
99
|
+
return content
|
|
100
|
+
}
|
|
101
|
+
throw lastErr ?? new Error('omlx unknown failure')
|
|
102
|
+
}
|
package/package.json
CHANGED
|
@@ -3,14 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Routing:
|
|
5
5
|
* 1. Cache lookup → hit → використати збережений verdict.
|
|
6
|
-
* 2. Cache miss → Tier 1 (
|
|
7
|
-
* 3. Tier 1 fail (
|
|
6
|
+
* 2. Cache miss → Tier 1 (resolveModel('min')) → parseVerdict.
|
|
7
|
+
* 3. Tier 1 fail (model error / bad JSON / Zod) → Tier 2 (CLOUD_MIN через pi).
|
|
8
8
|
* 4. Tier 2 fail → conservative fallback worth-testing/confidence=0.
|
|
9
|
+
*
|
|
10
|
+
* Бекенд обирається за model-id: `omlx/...` → прямий HTTP до omlx (локально),
|
|
11
|
+
* решта → pi CLI. Якщо omlx-Tier 1 недоступний, помилка падає в той самий catch
|
|
12
|
+
* і класифікація відкочується на хмарний Tier 2 через pi.
|
|
9
13
|
*/
|
|
10
14
|
import { spawnSync } from 'node:child_process'
|
|
11
15
|
import { join } from 'node:path'
|
|
12
16
|
|
|
13
17
|
import { CLOUD_MIN, resolveModel } from '../../lib/models.mjs'
|
|
18
|
+
import { callOmlx, isOmlxModel } from '../../lib/omlx.mjs'
|
|
14
19
|
import { deriveCacheKey, readCache, writeCache } from './cache.mjs'
|
|
15
20
|
import { buildUserPrompt, SYSTEM_PROMPT } from './prompt.mjs'
|
|
16
21
|
import { parseVerdict } from './verdict-schema.mjs'
|
|
@@ -22,13 +27,17 @@ const FALLBACK_VERDICT = {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
|
-
* Викликає
|
|
30
|
+
* Викликає LLM за model-id і повертає raw текст відповіді.
|
|
31
|
+
* `omlx/...` → прямий HTTP до omlx (text-only); решта → pi CLI.
|
|
26
32
|
* @param {string} prompt текст промпта
|
|
27
|
-
* @param {string} model provider/model-id або '' для pi-дефолту
|
|
28
|
-
* @returns {string}
|
|
29
|
-
* @throws якщо
|
|
33
|
+
* @param {string} model provider/model-id, `omlx/...` або '' для pi-дефолту
|
|
34
|
+
* @returns {string} текст відповіді моделі
|
|
35
|
+
* @throws якщо backend недоступний або повертає помилку
|
|
30
36
|
*/
|
|
31
|
-
function
|
|
37
|
+
function callModel(prompt, model) {
|
|
38
|
+
if (isOmlxModel(model)) {
|
|
39
|
+
return callOmlx([{ role: 'user', content: prompt }], model, { timeoutMs: 60_000 })
|
|
40
|
+
}
|
|
32
41
|
const modelArgs = model ? ['--model', model] : []
|
|
33
42
|
const r = spawnSync('pi', ['-p', prompt, ...modelArgs, '--no-session', '--mode', 'text', '--no-tools'], {
|
|
34
43
|
encoding: 'utf8',
|
|
@@ -44,21 +53,21 @@ function callPi(prompt, model) {
|
|
|
44
53
|
* @param {{file: string, mutants: object[]}} group група мутантів одного файлу
|
|
45
54
|
* @param {object} mutant конкретний мутант
|
|
46
55
|
* @param {string} cwd корінь проєкту
|
|
47
|
-
* @param {(prompt: string, model: string) => string}
|
|
56
|
+
* @param {(prompt: string, model: string) => string} callModelFn ін'єкція для тестів
|
|
48
57
|
* @returns {object} verdict класифікації
|
|
49
58
|
*/
|
|
50
|
-
function classifyOne(group, mutant, cwd,
|
|
59
|
+
function classifyOne(group, mutant, cwd, callModelFn) {
|
|
51
60
|
const prompt = `${SYSTEM_PROMPT}\n\n${buildUserPrompt({ ...mutant, file: group.file }, cwd)}`
|
|
52
61
|
const loc = `${group.file}:${mutant.line}:${mutant.col}`
|
|
53
62
|
|
|
54
63
|
// Tier 1: resolveModel('min') — каскад local→cloud якщо локалі нема
|
|
55
64
|
try {
|
|
56
|
-
const text =
|
|
65
|
+
const text = callModelFn(prompt, resolveModel('min'))
|
|
57
66
|
return parseVerdict(text)
|
|
58
67
|
} catch {
|
|
59
68
|
// Tier 2: CLOUD_MIN
|
|
60
69
|
try {
|
|
61
|
-
const text =
|
|
70
|
+
const text = callModelFn(prompt, CLOUD_MIN)
|
|
62
71
|
return parseVerdict(text)
|
|
63
72
|
} catch (error) {
|
|
64
73
|
console.warn(`⚠ coverage classify: ${loc} both tiers failed: ${error.message}`)
|
|
@@ -68,15 +77,15 @@ function classifyOne(group, mutant, cwd, callPiFn) {
|
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
/**
|
|
71
|
-
* Класифікує survived мутантів
|
|
80
|
+
* Класифікує survived мутантів (resolveModel('min') → CLOUD_MIN → fallback).
|
|
72
81
|
* @param {Array<{file: string, mutants: object[], exampleTest?: object|null, recommendationText?: string|null}>} survived список вцілілих мутантів
|
|
73
82
|
* @param {string} cwd корінь проєкту
|
|
74
|
-
* @param {{cachePath?: string,
|
|
83
|
+
* @param {{cachePath?: string, callModel?: Function}} [opts] ін'єкції для тестів
|
|
75
84
|
* @returns {Promise<Array<{key: string, verdict: object}>>} verdicts
|
|
76
85
|
*/
|
|
77
86
|
export function classify(survived, cwd, opts = {}) {
|
|
78
87
|
const cachePath = opts.cachePath ?? join(cwd, 'npm/reports/coverage-classify.cache.json')
|
|
79
|
-
const
|
|
88
|
+
const callModelFn = opts.callModel ?? callModel
|
|
80
89
|
const cacheModel = `${resolveModel('min') || 'default'}+${CLOUD_MIN || 'cloud'}`
|
|
81
90
|
|
|
82
91
|
const cache = readCache(cachePath)
|
|
@@ -102,7 +111,7 @@ export function classify(survived, cwd, opts = {}) {
|
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
if (!verdict) {
|
|
105
|
-
verdict = classifyOne(group, mutant, cwd,
|
|
114
|
+
verdict = classifyOne(group, mutant, cwd, callModelFn)
|
|
106
115
|
if (cacheKey) {
|
|
107
116
|
cache.entries[cacheKey] = { ...verdict, classifiedAt: new Date().toISOString() }
|
|
108
117
|
}
|
|
@@ -77,7 +77,7 @@ const pi = await runBackendAsync('pi')
|
|
|
77
77
|
function avg(a) { return a.length ? Math.round(a.reduce((x, y) => x + y, 0) / a.length) : 0 }
|
|
78
78
|
function median(a) {
|
|
79
79
|
if (!a.length) return 0
|
|
80
|
-
const s =
|
|
80
|
+
const s = a.toSorted((x, y) => x - y)
|
|
81
81
|
return s[Math.floor(s.length / 2)]
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -46,7 +46,7 @@ function uniq(arr) {
|
|
|
46
46
|
* }}
|
|
47
47
|
*/
|
|
48
48
|
export function extractAnchors(src) {
|
|
49
|
-
const urls = uniq(
|
|
49
|
+
const urls = uniq(Array.from(src.matchAll(URL_RE), m => m[0]))
|
|
50
50
|
|
|
51
51
|
const magicStrings = []
|
|
52
52
|
const seenNames = new Set()
|
|
@@ -59,12 +59,12 @@ export function extractAnchors(src) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const errorMarkers = uniq(
|
|
63
|
-
const configRefs = uniq(
|
|
62
|
+
const errorMarkers = uniq(Array.from(src.matchAll(ERROR_MARKER_RE), m => m[1]))
|
|
63
|
+
const configRefs = uniq(Array.from(src.matchAll(CONFIG_REF_RE), m => m[1]))
|
|
64
64
|
|
|
65
65
|
// Витягуємо code-block приклади тільки з file-header — там автор зазвичай показує контракт.
|
|
66
66
|
const headerMatch = src.match(FILE_HEADER_RE)
|
|
67
|
-
const examples = headerMatch ? uniq(
|
|
67
|
+
const examples = headerMatch ? uniq(Array.from(headerMatch[1].matchAll(CODE_BLOCK_RE), m => m[1].trim())) : []
|
|
68
68
|
|
|
69
69
|
return { urls, magicStrings, errorMarkers, configRefs, examples }
|
|
70
70
|
}
|
|
@@ -4,6 +4,7 @@ import { basename } from 'node:path'
|
|
|
4
4
|
import { spawnSync } from 'node:child_process'
|
|
5
5
|
import { env } from 'node:process'
|
|
6
6
|
import { resolveModel } from '../../../lib/models.mjs'
|
|
7
|
+
import { callOmlx } from '../../../lib/omlx.mjs'
|
|
7
8
|
import { extractFacts } from './docgen-extract.mjs'
|
|
8
9
|
import { extractAnchors } from './docgen-extract-anchors.mjs'
|
|
9
10
|
import { oneShotMessages, sectionMessages, criticMessages, refineMessages, guaranteesFromMarkers } from './docgen-prompts.mjs'
|
|
@@ -92,50 +93,16 @@ function scoreDoc(md, facts) {
|
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
95
|
* omlx-бекенд: справжні OpenAI-сумісні messages (system+user збереженi).
|
|
95
|
-
* Вмикається `N_CURSOR_DOCGEN_BACKEND=omlx`.
|
|
96
|
-
*
|
|
97
|
-
* Модель: переданий `model`, потім `N_CURSOR_DOCGEN_OMLX_MODEL`, потім дефолт.
|
|
96
|
+
* Вмикається `N_CURSOR_DOCGEN_BACKEND=omlx`. Делегує у спільний `callOmlx`
|
|
97
|
+
* (npm/lib/omlx.mjs) з docgen-специфічними env-дефолтами URL/моделі.
|
|
98
98
|
*/
|
|
99
99
|
function callOmlxMessages(messages, model, timeoutMs, temperature = 0.2) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
max_tokens: 4096,
|
|
106
|
-
temperature
|
|
100
|
+
return callOmlx(messages, model, {
|
|
101
|
+
url: env.N_CURSOR_DOCGEN_OMLX_URL,
|
|
102
|
+
timeoutMs,
|
|
103
|
+
temperature,
|
|
104
|
+
fallbackModel: env.N_CURSOR_DOCGEN_OMLX_MODEL
|
|
107
105
|
})
|
|
108
|
-
// Ретраїмо лише transient curl-помилки (18 = transfer closed, 56 = recv failure, 52 = empty reply).
|
|
109
|
-
const TRANSIENT_CURL_CODES = new Set([18, 52, 56])
|
|
110
|
-
let lastErr
|
|
111
|
-
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
112
|
-
const r = spawnSync(
|
|
113
|
-
'curl',
|
|
114
|
-
['-sS', '-X', 'POST', url, '-H', 'Content-Type: application/json', '-H', 'Connection: close', '--max-time', String(Math.ceil(timeoutMs / 1000)), '--data-binary', '@-'],
|
|
115
|
-
{ input: body, encoding: 'utf8', timeout: timeoutMs + 5000 }
|
|
116
|
-
)
|
|
117
|
-
if (r.error) {
|
|
118
|
-
lastErr = new Error(`omlx curl error: ${r.error.message}`)
|
|
119
|
-
break
|
|
120
|
-
}
|
|
121
|
-
if (r.status !== 0) {
|
|
122
|
-
if (TRANSIENT_CURL_CODES.has(r.status) && attempt < 3) {
|
|
123
|
-
lastErr = new Error(`omlx curl exit ${r.status} (transient, retry ${attempt})`)
|
|
124
|
-
continue
|
|
125
|
-
}
|
|
126
|
-
throw new Error(`omlx curl exit ${r.status}: ${r.stderr?.slice(0, 300) ?? ''}`)
|
|
127
|
-
}
|
|
128
|
-
let j
|
|
129
|
-
try { j = JSON.parse(r.stdout) } catch { throw new Error(`omlx bad json: ${r.stdout?.slice(0, 200) ?? ''}`) }
|
|
130
|
-
if (j.error) throw new Error(`omlx api: ${JSON.stringify(j.error).slice(0, 300)}`)
|
|
131
|
-
const content = j.choices?.[0]?.message?.content?.trim() ?? ''
|
|
132
|
-
if (!content) {
|
|
133
|
-
const finish = j.choices?.[0]?.finish_reason
|
|
134
|
-
throw new Error(`omlx empty content (finish=${finish})`)
|
|
135
|
-
}
|
|
136
|
-
return content
|
|
137
|
-
}
|
|
138
|
-
throw lastErr ?? new Error('omlx unknown failure')
|
|
139
106
|
}
|
|
140
107
|
|
|
141
108
|
/**
|
|
@@ -5,6 +5,7 @@ import { join } from 'node:path'
|
|
|
5
5
|
import { spawnSync } from 'node:child_process'
|
|
6
6
|
import { env } from 'node:process'
|
|
7
7
|
import { resolveModel } from '../../../lib/models.mjs'
|
|
8
|
+
import { callOmlx, isOmlxModel } from '../../../lib/omlx.mjs'
|
|
8
9
|
|
|
9
10
|
// Тир за замовчуванням: min → avg при ескалації (каскад local→cloud).
|
|
10
11
|
// Перевизначення через N_CURSOR_FIX_MODEL / N_CURSOR_FIX_MODEL_HEAVY.
|
|
@@ -86,12 +87,20 @@ function buildPrompt(ruleId, ruleMdc, output, files) {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
/**
|
|
89
|
-
*
|
|
90
|
+
* Викликає LLM за model-id і повертає текст відповіді.
|
|
91
|
+
* `omlx/...` → прямий HTTP до omlx (text-only, локально); решта → pi CLI.
|
|
90
92
|
* @param {string} prompt текст промпта
|
|
91
|
-
* @param {string} model назва моделі (provider/id)
|
|
92
|
-
* @returns {{ text: string, error?: string }}
|
|
93
|
+
* @param {string} model назва моделі (provider/id, `omlx/...` або '')
|
|
94
|
+
* @returns {{ text: string, error?: string }} текст відповіді або повідомлення про помилку
|
|
93
95
|
*/
|
|
94
|
-
function
|
|
96
|
+
function callModel(prompt, model) {
|
|
97
|
+
if (isOmlxModel(model)) {
|
|
98
|
+
try {
|
|
99
|
+
return { text: callOmlx([{ role: 'user', content: prompt }], model, { timeoutMs: 120_000 }) }
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return { text: '', error: error.message }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
95
104
|
const modelArgs = model ? ['--model', model] : []
|
|
96
105
|
const r = spawnSync('pi', ['-p', prompt, ...modelArgs, '--no-session', '--mode', 'text', '--no-tools'], {
|
|
97
106
|
encoding: 'utf8',
|
|
@@ -117,9 +126,9 @@ function callPi(prompt, model) {
|
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
/**
|
|
120
|
-
* Парсить JSON-відповідь від
|
|
121
|
-
*
|
|
122
|
-
* @param {string} text сирий
|
|
129
|
+
* Парсить JSON-відповідь від моделі.
|
|
130
|
+
* Модель може обгорнути JSON у ```json ... ```, тому пробуємо витягти.
|
|
131
|
+
* @param {string} text сирий текст відповіді
|
|
123
132
|
* @returns {{ changes: Array<{path:string,content:string}>, error?: string } | null} розпарсений патч або null
|
|
124
133
|
*/
|
|
125
134
|
function parseResponse(text) {
|
|
@@ -183,12 +192,12 @@ export function runLlmWorker(ruleId, violationOutput, projectRoot, opts = {}) {
|
|
|
183
192
|
})
|
|
184
193
|
.filter(Boolean)
|
|
185
194
|
|
|
186
|
-
// 3. Будуємо prompt і викликаємо
|
|
195
|
+
// 3. Будуємо prompt і викликаємо модель
|
|
187
196
|
const prompt = buildPrompt(ruleId, ruleMdc, violationOutput, files)
|
|
188
|
-
const { text, error:
|
|
197
|
+
const { text, error: modelError } = callModel(prompt, model)
|
|
189
198
|
|
|
190
|
-
if (
|
|
191
|
-
if (!text) return { ok: false, error: '
|
|
199
|
+
if (modelError) return { ok: false, error: modelError }
|
|
200
|
+
if (!text) return { ok: false, error: 'model returned empty response' }
|
|
192
201
|
|
|
193
202
|
// 4. Парсимо відповідь
|
|
194
203
|
const parsed = parseResponse(text)
|