@nitra/cursor 5.2.0 → 5.3.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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.3.0] - 2026-06-11
4
+
5
+ ### Added
6
+
7
+ - js-lint: нові JS-файли створюються з явним розширенням .mjs/.cjs (не .js); приклади нового вихідного коду в js-run/js-bun-db/vue переведено на .mjs. test: підтримка vitest.config.mjs — pool-forks і stryker_config приймають .mjs/.js (новий канон .mjs, legacy .js не дублюється), stryker.configFile приводиться до фактичного імені. style-lint: чекер розпізнає stylelint.config.mjs/.cjs та .stylelintrc.mjs/.cjs.
8
+ - llm wire-trace: always-on багатий JSONL-запис на кожен `callLlm` (обидва канали — reasoning + слід) у `<cwd>/.n-cursor/llm-trace.jsonl` (gitignored, недеструктивна ротація 50 MB, kill-switch `N_CURSOR_LLM_TRACE=0`); `callOmlxRaw` дістає reasoning_content/usage/finish_reason/attempts (`callOmlx` лишається `string`-обгорткою); `fix`+`coverage-classify` мігровано з прямого `callOmlx`/`pi`-spawn на спільний `callLlm` (caller-мітка). Спека: docs/specs/2026-06-10-omlx-wire-trace-capture-design.md
9
+
10
+ ## [5.2.1] - 2026-06-11
11
+
12
+ ### Changed
13
+
14
+ - adr
15
+
3
16
  ## [5.2.0] - 2026-06-11
4
17
 
5
18
  ### Changed
@@ -1,30 +1,37 @@
1
+ ---
2
+ docgen:
3
+ source: npm/lib/models.mjs
4
+ crc: feb82992
5
+ score: 100
6
+ ---
7
+
1
8
  # models.mjs
2
9
 
3
10
  ## Огляд
4
11
 
5
- Файл визначає ієрархічну класифікацію моделей для системи pi. Класифікація встановлює зв'язок між локальними та хмарними провайдерами. Функція resolveModel забезпечує маршрутизацію вибору моделі залежно від заданого рівня доступності.
12
+ Файл визначає глобальну класифікацію моделей для системи pi, встановлюючи конфігураційні моделі для локального та хмарного інференсу через змінні середовища (наприклад, `N_LOCAL_MIN_MODEL`). Значення моделі мають формат "provider/model-id".
13
+
14
+ Система надає механізм каскадного вибору моделі через функцію `resolveModel`. Цей механізм послідовно перевіряє локальні тири (`LOCAL_MIN` $\rightarrow$ `LOCAL_AVG` $\rightarrow$ `LOCAL_MAX`), а потім хмарні тири, якщо попередні не визначені. Це забезпечує прозору роботу, навіть якщо локальні моделі відсутні. Прямі константи (наприклад, `LOCAL_MIN`) залишені для випадків, що вимагають явного контролю над вибором моделі.
6
15
 
7
16
  ## Поведінка
8
17
 
9
- LOCAL_MIN встановлює мінімальний локальний провайдер
10
- LOCAL_AVG встановлює середній локальний провайдер
11
- LOCAL_MAX встановлює максимальний локальний провайдер
12
- CLOUD_MIN встановлює мінімальний хмарний провайдер
13
- CLOUD_AVG встановлює середній хмарний провайдер
14
- CLOUD_MAX встановлює максимальний хмарний провайдер
15
- resolveModel повертає перший непорожній model-id з каскадного перевірки локальних та хмарних провайдерів
16
- resolveModel приймає тир min avg або max
17
- resolveModel повертає model-id або порожній рядок якщо жоден тир не задано
18
+ LOCAL_MIN повертає модель для швидкого локального інференсу, або порожній рядок, якщо змінна середовища не встановлена.
19
+ LOCAL_AVG повертає модель для середнього локального інференсу, або порожній рядок, якщо змінна середовища не встановлена.
20
+ LOCAL_MAX повертає модель для максимального локального інференсу, або порожній рядок, якщо змінна середовища не встановлена.
21
+ CLOUD_MIN повертає модель для мінімального хмарного інференсу, або порожній рядок, якщо змінна середовища не встановлена.
22
+ CLOUD_AVG повертає модель для середнього хмарного інференсу, або порожній рядок, якщо змінна середовища не встановлена.
23
+ CLOUD_MAX повертає модель для максимального хмарного інференсу, або порожній рядок, якщо змінна середовища не встановлена.
24
+ resolveModel повертає перший непорожній model-id для запитаного тиру, каскадно перевіряючи локальні тири, а потім хмарний еквівалент, або порожній рядок, якщо жоден тир не задано.
18
25
 
19
26
  ## Публічний API
20
27
 
21
- LOCAL_MIN — Виконує швидкий локальний inference.
22
- LOCAL_AVG — Виконує середній локальний inference.
23
- LOCAL_MAX — Виконує максимальний локальний inference.
24
- CLOUD_MIN — Виконує мінімальний хмарний inference.
25
- CLOUD_AVG — Виконує середній хмарний inference.
26
- CLOUD_MAX — Виконує максимальний хмарний inference.
27
- resolveModel — Повертає перший непорожній model-id для запиту, перевіряючи спочатку локальні, а потім хмарні варіанти.
28
+ LOCAL_MIN — Швидке виконання моделі на локальному пристрої.
29
+ LOCAL_AVG — Середнє за продуктивністю виконання моделі на локальному пристрої.
30
+ LOCAL_MAX — Найпотужніше виконання моделі на локальному пристрої.
31
+ CLOUD_MIN — Найменш ресурсомістке виконання моделі в хмарі.
32
+ CLOUD_AVG — Середній рівень продуктивності виконання моделі в хмарі.
33
+ CLOUD_MAX — Найпотужніше виконання моделі в хмарі.
34
+ resolveModel — Знаходить і повертає ідентифікатор моделі, починаючи з локальних варіантів, а потім переходячи до хмарних відповідників.
28
35
 
29
36
  ## Гарантії поведінки
30
37
 
package/lib/llm.mjs CHANGED
@@ -7,15 +7,16 @@
7
7
  *
8
8
  * Жодних env-перемикачів бекенда: рядок моделі сам визначає транспорт.
9
9
  *
10
- * Wire-trace (ADR 260610-1516/1524): якщо виставлено `N_CURSOR_LLM_TRACE=<file>`,
11
- * кожен виклик append-ить один JSONL-рядок з бекендом, моделлю, тривалістю і
12
- * розмірами prompt/output. Трейс fail-safe: помилка запису не ламає виклик.
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`.
13
14
  */
14
15
  import { spawnSync } from 'node:child_process'
15
- import { appendFileSync } from 'node:fs'
16
16
  import { env } from 'node:process'
17
17
 
18
- import { callOmlx, isOmlxModel } from './omlx.mjs'
18
+ import { callOmlxRaw, isOmlxModel } from './omlx.mjs'
19
+ import { buildTraceRecord, writeTrace } from './omlx-trace.mjs'
19
20
 
20
21
  /** Дефолтний timeout одного виклику (узгоджено з LOCAL_TIMEOUT доки-конвеєра). */
21
22
  const DEFAULT_TIMEOUT_MS = 120_000
@@ -29,20 +30,6 @@ export function pickBackend(model) {
29
30
  return isOmlxModel(model) ? 'omlx' : 'pi'
30
31
  }
31
32
 
32
- /**
33
- * Fail-safe append JSONL-рядка трейсу у файл з `N_CURSOR_LLM_TRACE`.
34
- * @param {object} entry один запис трейсу
35
- */
36
- function trace(entry) {
37
- const file = env.N_CURSOR_LLM_TRACE
38
- if (!file) return
39
- try {
40
- appendFileSync(file, JSON.stringify(entry) + '\n')
41
- } catch {
42
- // трейс не має ламати основний виклик
43
- }
44
- }
45
-
46
33
  /**
47
34
  * Виклик через `pi` CLI: messages конкатенуються у plain prompt
48
35
  * (pi не приймає messages-масив), tools вимкнено.
@@ -64,42 +51,68 @@ function callPi(messages, model, timeoutMs) {
64
51
  }
65
52
 
66
53
  /**
67
- * Універсальний LLM-виклик з маршрутизацією за префіксом model-id.
54
+ * Універсальний LLM-виклик з маршрутизацією за префіксом model-id і always-on
55
+ * wire-trace (обидва канали).
68
56
  * @param {Array<{role:string, content:string}>} messages OpenAI-style messages (system зберігається на omlx)
69
57
  * @param {string} model model-id; `omlx/<m>` → прямий HTTP, інакше → pi CLI
70
- * @param {{ timeoutMs?: number, temperature?: number, maxTokens?: number, url?: string }} [opts] timeout, температура, ліміт виходу, override URL
58
+ * @param {{ timeoutMs?: number, temperature?: number, maxTokens?: number, url?: string, caller?: string }} [opts] timeout, температура, ліміт виходу, override URL, мітка викликача для trace
71
59
  * @returns {string} текст відповіді (непорожній на omlx; pi може повернути '')
72
60
  */
73
61
  export function callLlm(messages, model, opts = {}) {
74
- const { timeoutMs = DEFAULT_TIMEOUT_MS, temperature = 0.2, maxTokens, url } = opts
62
+ const { timeoutMs = DEFAULT_TIMEOUT_MS, temperature = 0.2, maxTokens, url, caller } = opts
75
63
  const backend = pickBackend(model)
64
+ const resolvedCaller = caller ?? env.N_CURSOR_TRACE_CALLER ?? 'unknown'
76
65
  const t0 = Date.now()
77
- const promptChars = messages.reduce((n, m) => n + (m.content?.length ?? 0), 0)
78
66
  try {
79
- const out =
80
- backend === 'omlx'
81
- ? callOmlx(messages, model, { url, timeoutMs, temperature, ...(maxTokens ? { maxTokens } : {}) })
82
- : callPi(messages, model, timeoutMs)
83
- trace({
84
- ts: new Date().toISOString(),
85
- backend,
86
- model,
87
- ms: Date.now() - t0,
88
- promptChars,
89
- outChars: out.length,
90
- ok: true
91
- })
92
- return out
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, { url, timeoutMs, temperature, ...(maxTokens ? { maxTokens } : {}) })
75
+ ;({ content, reasoning, reasoningSource, finishReason, usage, attempts } = raw)
76
+ } else {
77
+ content = callPi(messages, model, timeoutMs)
78
+ }
79
+ writeTrace(
80
+ buildTraceRecord({
81
+ ts: new Date().toISOString(),
82
+ caller: resolvedCaller,
83
+ backend,
84
+ model,
85
+ temperature,
86
+ maxTokens,
87
+ messages,
88
+ content,
89
+ reasoning,
90
+ reasoningSource,
91
+ finishReason,
92
+ usage,
93
+ ms: Date.now() - t0,
94
+ attempts,
95
+ ok: true,
96
+ error: null
97
+ })
98
+ )
99
+ return content
93
100
  } catch (error) {
94
- trace({
95
- ts: new Date().toISOString(),
96
- backend,
97
- model,
98
- ms: Date.now() - t0,
99
- promptChars,
100
- ok: false,
101
- error: String(error.message).slice(0, 200)
102
- })
101
+ writeTrace(
102
+ buildTraceRecord({
103
+ ts: new Date().toISOString(),
104
+ caller: resolvedCaller,
105
+ backend,
106
+ model,
107
+ temperature,
108
+ maxTokens,
109
+ messages,
110
+ ms: Date.now() - t0,
111
+ attempts: null,
112
+ ok: false,
113
+ error: String(error.message).slice(0, 200)
114
+ })
115
+ )
103
116
  throw error
104
117
  }
105
118
  }
@@ -124,7 +137,7 @@ const AUTH_ERROR_MARKER = 'authentication_error'
124
137
  export function omlxHealthCheck(opts = {}) {
125
138
  const { url, model = '', timeoutMs = DEFAULT_TIMEOUT_MS } = opts
126
139
  try {
127
- callOmlx([{ role: 'user', content: 'ok' }], model, { url, timeoutMs, maxTokens: 1, temperature: 0 })
140
+ callOmlxRaw([{ role: 'user', content: 'ok' }], model, { url, timeoutMs, maxTokens: 1, temperature: 0 })
128
141
  return { ok: true, reason: null, detail: '' }
129
142
  } catch (error) {
130
143
  const detail = String(error.message)
@@ -0,0 +1,158 @@
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
+ }
package/lib/omlx.mjs CHANGED
@@ -70,16 +70,42 @@ export function omlxModelId(model) {
70
70
  }
71
71
 
72
72
  /**
73
- * Прямий HTTP-виклик до omlx через `curl` (spawnSync). Повертає текст
74
- * `choices[0].message.content`. Ретраїть лише transient curl-помилки
75
- * (18 = transfer closed, 52 = empty reply, 56 = recv failure).
73
+ * Витягує reasoning (думки моделі) з omlx-`message`. Джерела за пріоритетом:
74
+ * - `field` — окреме поле `message.reasoning_content` (Qwen3-Thinking тощо);
75
+ * - `think_tag` `<think>…</think>` усередині `content` (інші thinking-моделі);
76
+ * - `truncated` — `finish_reason: "length"` зрізав думку в `content` до закриття
77
+ * тега → сирий reasoning лишився в `content` без `</think>`;
78
+ * - `null` — reasoning немає (не-thinking модель).
79
+ * @param {{content?:string, reasoning_content?:string}} message обʼєкт `choices[0].message`
80
+ * @param {string|null} finishReason `choices[0].finish_reason`
81
+ * @returns {{ reasoning: string|null, reasoningSource: 'field'|'think_tag'|'truncated'|null }} текст думок і його джерело
82
+ */
83
+ const THINK_TAG_RE = /<think>([\s\S]*?)<\/think>/
84
+
85
+ /**
86
+ *
87
+ */
88
+ export function extractReasoning(message, finishReason) {
89
+ const field = message?.reasoning_content
90
+ if (field && field.trim()) return { reasoning: field, reasoningSource: 'field' }
91
+ const content = message?.content ?? ''
92
+ const m = content.match(THINK_TAG_RE)
93
+ if (m) return { reasoning: m[1].trim(), reasoningSource: 'think_tag' }
94
+ if (finishReason === 'length' && content.trim()) return { reasoning: content, reasoningSource: 'truncated' }
95
+ return { reasoning: null, reasoningSource: null }
96
+ }
97
+
98
+ /**
99
+ * Ядро прямого HTTP-виклику до omlx через `curl` (spawnSync). Повертає **багатий**
100
+ * обʼєкт: контент + reasoning + usage + finish_reason + кількість спроб. Ретраїть
101
+ * лише transient curl-помилки (18 = transfer closed, 52 = empty reply, 56 = recv failure).
76
102
  * @param {Array<{role:string, content:string}>} messages OpenAI-messages (system+user збережено)
77
103
  * @param {string} model model-id (з/без `omlx/`-префікса); порожній → дефолт
78
104
  * @param {{ url?: string, timeoutMs?: number, temperature?: number, maxTokens?: number, fallbackModel?: string, apiKey?: string }} [opts] URL, timeout, температура, ліміт виходу, fallback-модель, API-ключ
79
- * @returns {string} непорожній контент відповіді
105
+ * @returns {{ content:string, reasoning:string|null, reasoningSource:string|null, finishReason:string|null, usage:object|null, attempts:number }} багатий результат виклику
80
106
  * @throws на curl-помилці, не-200 exit, поганому JSON чи порожньому контенті
81
107
  */
82
- export function callOmlx(messages, model, opts = {}) {
108
+ export function callOmlxRaw(messages, model, opts = {}) {
83
109
  const {
84
110
  url = env.N_CURSOR_OMLX_URL ?? DEFAULT_OMLX_URL,
85
111
  timeoutMs = 60_000,
@@ -136,12 +162,24 @@ export function callOmlx(messages, model, opts = {}) {
136
162
  throw new Error(`omlx bad json: ${r.stdout?.slice(0, 200) ?? ''}`)
137
163
  }
138
164
  if (j.error) throw new Error(`omlx api: ${JSON.stringify(j.error).slice(0, 300)}`)
139
- const content = j.choices?.[0]?.message?.content?.trim() ?? ''
140
- if (!content) {
141
- const finish = j.choices?.[0]?.finish_reason
142
- throw new Error(`omlx empty content (finish=${finish})`)
143
- }
144
- return content
165
+ const message = j.choices?.[0]?.message ?? {}
166
+ const finishReason = j.choices?.[0]?.finish_reason ?? null
167
+ const content = message.content?.trim() ?? ''
168
+ if (!content) throw new Error(`omlx empty content (finish=${finishReason})`)
169
+ const { reasoning, reasoningSource } = extractReasoning(message, finishReason)
170
+ return { content, reasoning, reasoningSource, finishReason, usage: j.usage ?? null, attempts: attempt }
145
171
  }
146
172
  throw lastErr ?? new Error('omlx unknown failure')
147
173
  }
174
+
175
+ /**
176
+ * Тонка обгортка над `callOmlxRaw` для споживачів, яким потрібен лише текст.
177
+ * Контракт незмінний: повертає непорожній `choices[0].message.content`.
178
+ * @param {Array<{role:string, content:string}>} messages OpenAI-messages
179
+ * @param {string} model model-id (з/без `omlx/`-префікса)
180
+ * @param {object} [opts] ті самі опції, що й у `callOmlxRaw`
181
+ * @returns {string} непорожній контент відповіді
182
+ */
183
+ export function callOmlx(messages, model, opts = {}) {
184
+ return callOmlxRaw(messages, model, opts).content
185
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",