tensorgrid-ui 2.0.0 → 2.1.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/lib/client.js +93 -17
- package/lib/http-runtime.js +96 -7
- package/lib/reviewers.js +7 -2
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -198,6 +198,8 @@ window.__ModuleLoader__.load({
|
|
|
198
198
|
'review.bySubscription': 'subscription',
|
|
199
199
|
'review.limit': 'That is as many reviewers as one review can hold.',
|
|
200
200
|
'review.byApiKey': 'paid per token',
|
|
201
|
+
'review.groupSubscription': 'Included in a subscription',
|
|
202
|
+
'review.groupPaid': 'Paid separately \u2014 per token',
|
|
201
203
|
'review.nav': 'Reviewers',
|
|
202
204
|
'review.title': 'Second opinion',
|
|
203
205
|
'review.hint': 'Independent reviewers examine finished work and report what is wrong. Choose how many and which ones: different models find different things.',
|
|
@@ -274,6 +276,8 @@ window.__ModuleLoader__.load({
|
|
|
274
276
|
'review.bySubscription': '订阅',
|
|
275
277
|
'review.limit': '一次审阅最多只能有这么多审阅者。',
|
|
276
278
|
'review.byApiKey': '按量计费',
|
|
279
|
+
'review.groupSubscription': '包含在订阅中',
|
|
280
|
+
'review.groupPaid': '单独计费 \u2014 按量',
|
|
277
281
|
'review.nav': '审阅者',
|
|
278
282
|
'review.title': '第二意见',
|
|
279
283
|
'review.hint': '独立审阅者检查已完成的工作并报告问题。自行决定数量与人选:不同模型发现的问题不同。',
|
|
@@ -1647,6 +1651,8 @@ window.__ModuleLoader__.load({
|
|
|
1647
1651
|
'review.bySubscription': 'по подписке',
|
|
1648
1652
|
'review.limit': 'Больше ревизоров в один обзор не поместится.',
|
|
1649
1653
|
'review.byApiKey': 'по токенам',
|
|
1654
|
+
'review.groupSubscription': 'Входят в подписку',
|
|
1655
|
+
'review.groupPaid': 'Оплата отдельно \u2014 по токенам',
|
|
1650
1656
|
'review.nav': 'Ревизоры',
|
|
1651
1657
|
'review.title': 'Второе мнение',
|
|
1652
1658
|
'review.hint': 'Независимые ревизоры смотрят готовую работу и сообщают, что в ней не так. Сколько их и какие — выбираете вы: разные модели находят разное.',
|
|
@@ -2333,13 +2339,12 @@ window.__ModuleLoader__.load({
|
|
|
2333
2339
|
/**
|
|
2334
2340
|
* Выбор значения: известные варианты плюс «своё».
|
|
2335
2341
|
*
|
|
2336
|
-
*
|
|
2337
|
-
*
|
|
2338
|
-
*
|
|
2339
|
-
*
|
|
2340
|
-
* интерфейс бесполезным до следующего обновления пакета.
|
|
2342
|
+
* `groups` разносит варианты по подписям. Для моделей это принципиально:
|
|
2343
|
+
* один и тот же `claude-opus-5` доступен и по подписке, и через
|
|
2344
|
+
* OpenRouter по токенам, и вперемешку они неразличимы — а разница в том,
|
|
2345
|
+
* придёт ли за них счёт.
|
|
2341
2346
|
*/
|
|
2342
|
-
function ValuePicker({ t, value, options, disabled, onPick, placeholder }) {
|
|
2347
|
+
function ValuePicker({ t, value, options, disabled, onPick, placeholder, groups }) {
|
|
2343
2348
|
const known = options.some((option) => option.value === value)
|
|
2344
2349
|
const [custom, setCustom] = React.useState(value !== null && !known)
|
|
2345
2350
|
|
|
@@ -2367,6 +2372,17 @@ window.__ModuleLoader__.load({
|
|
|
2367
2372
|
})
|
|
2368
2373
|
}
|
|
2369
2374
|
|
|
2375
|
+
// Группы — это `optgroup`, а не подписи-разделители внутри списка:
|
|
2376
|
+
// браузер сам не даст выбрать заголовок и показывает его иначе.
|
|
2377
|
+
const body = groups === undefined
|
|
2378
|
+
? options.map((option) => jsx('option', { value: option.value, children: option.label }, option.value))
|
|
2379
|
+
: groups
|
|
2380
|
+
.filter((group) => group.options.length > 0)
|
|
2381
|
+
.map((group) => jsx('optgroup', {
|
|
2382
|
+
label: group.label,
|
|
2383
|
+
children: group.options.map((option) => jsx('option', { value: option.value, children: option.label }, option.value)),
|
|
2384
|
+
}, group.label))
|
|
2385
|
+
|
|
2370
2386
|
return jsxs('select', {
|
|
2371
2387
|
className: 'dsx-rev__select',
|
|
2372
2388
|
value: value === null ? '' : value,
|
|
@@ -2377,7 +2393,7 @@ window.__ModuleLoader__.load({
|
|
|
2377
2393
|
},
|
|
2378
2394
|
children: [
|
|
2379
2395
|
jsx('option', { value: '', children: t('review.asConfigured') }, ''),
|
|
2380
|
-
...
|
|
2396
|
+
...body,
|
|
2381
2397
|
jsx('option', { value: '\u0000custom', children: t('review.other') }, 'custom'),
|
|
2382
2398
|
],
|
|
2383
2399
|
})
|
|
@@ -2461,6 +2477,21 @@ window.__ModuleLoader__.load({
|
|
|
2461
2477
|
|
|
2462
2478
|
const catalogue = state !== null && Array.isArray(state.catalogue) ? state.catalogue : []
|
|
2463
2479
|
const journal = state !== null && state.journal ? state.journal : null
|
|
2480
|
+
const effortMap = state !== null && state.efforts ? state.efforts : {}
|
|
2481
|
+
|
|
2482
|
+
/**
|
|
2483
|
+
* Уровни усилия для записи.
|
|
2484
|
+
*
|
|
2485
|
+
* У наших ревизоров приходят с Host по выбранной модели; у внешних
|
|
2486
|
+
* продуктов остаются зашитыми — спросить у них нечем.
|
|
2487
|
+
*/
|
|
2488
|
+
const effortsFor = (entry) => {
|
|
2489
|
+
const spec = catalogue.find((item) => item.kind === entry.kind)
|
|
2490
|
+
if (spec && !spec.internal) return spec.efforts || []
|
|
2491
|
+
if (entry.provider === null || entry.model === null) return []
|
|
2492
|
+
const info = effortMap[entry.provider + '/' + entry.model]
|
|
2493
|
+
return info && Array.isArray(info.efforts) ? info.efforts : []
|
|
2494
|
+
}
|
|
2464
2495
|
const reviewers = state !== null && state.settings && Array.isArray(state.settings.reviewers)
|
|
2465
2496
|
? state.settings.reviewers
|
|
2466
2497
|
: []
|
|
@@ -2487,6 +2518,18 @@ window.__ModuleLoader__.load({
|
|
|
2487
2518
|
}])
|
|
2488
2519
|
}
|
|
2489
2520
|
|
|
2521
|
+
/**
|
|
2522
|
+
* Отметка на карточке: по подписке, за отдельные деньги или молчание.
|
|
2523
|
+
*
|
|
2524
|
+
* ТРИ состояния, и третье обязательно. Прежде их было два, и всё, что
|
|
2525
|
+
* не подписка, объявлялось платным — включая случай «сказать нечего».
|
|
2526
|
+
* Получалось, что человек видел «по токенам» на модели, которая ему
|
|
2527
|
+
* давно оплачена, а внешний продукт с собственной подпиской получал
|
|
2528
|
+
* ту же метку.
|
|
2529
|
+
*
|
|
2530
|
+
* Молчание честнее догадки: отсутствие отметки означает «не знаю», и
|
|
2531
|
+
* это ровно то, что есть на самом деле.
|
|
2532
|
+
*/
|
|
2490
2533
|
const billingLabel = (kind) => {
|
|
2491
2534
|
if (kind === 'subscription') return t('review.bySubscription')
|
|
2492
2535
|
if (kind === 'api-key') return t('review.byApiKey')
|
|
@@ -2555,15 +2598,40 @@ window.__ModuleLoader__.load({
|
|
|
2555
2598
|
value: entry.model,
|
|
2556
2599
|
disabled: busy,
|
|
2557
2600
|
placeholder: t('review.modelName'),
|
|
2558
|
-
options: models.map((m) => ({
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2601
|
+
options: models.map((m) => ({ value: m.id, label: m.id })),
|
|
2602
|
+
// РАЗДЕЛЕНИЕ ПО ОПЛАТЕ.
|
|
2603
|
+
//
|
|
2604
|
+
// Один и тот же claude-opus-5 доступен и по
|
|
2605
|
+
// подписке, и через OpenRouter по токенам. В общем
|
|
2606
|
+
// списке они неразличимы, а разница в том, придёт
|
|
2607
|
+
// ли за них счёт. Провайдер стоит в названии: у
|
|
2608
|
+
// OpenRouter сотни моделей, и без него не понять,
|
|
2609
|
+
// чья она.
|
|
2610
|
+
// Групп ДВЕ, а не три. Различие, которое важно
|
|
2611
|
+
// человеку, одно: придёт ли за эту модель
|
|
2612
|
+
// отдельный счёт. Ключ, сохранённый в dsh, и
|
|
2613
|
+
// ключ из окружения — с этой стороны одно и то
|
|
2614
|
+
// же, а третья группа оказывалась почти пустой и
|
|
2615
|
+
// только мешала выбирать.
|
|
2616
|
+
// Группы появляются, ТОЛЬКО когда признак оплаты
|
|
2617
|
+
// действительно известен. Иначе — простой список:
|
|
2618
|
+
// деление на «подписка» и «по токенам», в котором
|
|
2619
|
+
// первая корзина пуста, а вторая содержит всё,
|
|
2620
|
+
// сообщает неправду, а не отсутствие сведений.
|
|
2621
|
+
groups: models.some((m) => m.billing !== null && m.billing !== undefined)
|
|
2622
|
+
? [
|
|
2623
|
+
{
|
|
2624
|
+
label: t('review.groupSubscription'),
|
|
2625
|
+
options: models.filter((m) => m.billing === 'subscription')
|
|
2626
|
+
.map((m) => ({ value: m.id, label: m.provider + ' \u00b7 ' + m.id })),
|
|
2627
|
+
},
|
|
2628
|
+
{
|
|
2629
|
+
label: t('review.groupPaid'),
|
|
2630
|
+
options: models.filter((m) => m.billing !== 'subscription')
|
|
2631
|
+
.map((m) => ({ value: m.id, label: (m.provider === null ? '' : m.provider + ' \u00b7 ') + m.id })),
|
|
2632
|
+
},
|
|
2633
|
+
]
|
|
2634
|
+
: undefined,
|
|
2567
2635
|
onPick: (value) => {
|
|
2568
2636
|
const found = models.find((m) => m.id === value)
|
|
2569
2637
|
patch(entry.id, {
|
|
@@ -2583,7 +2651,15 @@ window.__ModuleLoader__.load({
|
|
|
2583
2651
|
value: entry.effort,
|
|
2584
2652
|
disabled: busy,
|
|
2585
2653
|
placeholder: t('review.effortName'),
|
|
2586
|
-
|
|
2654
|
+
// Уровни у КАЖДОЙ МОДЕЛИ свои, и зашитый список
|
|
2655
|
+
// врал почти везде. Проверено: у claude-opus-5 их
|
|
2656
|
+
// шесть, у deepseek-v4-pro — три, а одна и та же
|
|
2657
|
+
// модель через подписку и через OpenRouter даёт
|
|
2658
|
+
// разные наборы.
|
|
2659
|
+
//
|
|
2660
|
+
// Пока модель не выбрана, спрашивать нечего:
|
|
2661
|
+
// остаётся ввод своего значения.
|
|
2662
|
+
options: effortsFor(entry).map((e) => ({ value: e, label: e })),
|
|
2587
2663
|
onPick: (value) => patch(entry.id, { effort: value }),
|
|
2588
2664
|
}),
|
|
2589
2665
|
],
|
package/lib/http-runtime.js
CHANGED
|
@@ -215,6 +215,43 @@ export async function handleUpdate(req, res, state, loadModule) {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Уровни усилия ДЛЯ КОНКРЕТНОЙ МОДЕЛИ.
|
|
220
|
+
*
|
|
221
|
+
* Зашитый список был неверен почти для всех. Проверено на живых данных:
|
|
222
|
+
*
|
|
223
|
+
* anthropic/claude-opus-5 minimal, low, medium, high, xhigh, max
|
|
224
|
+
* anthropic/claude-sonnet-4-5 off, minimal, low, medium, high
|
|
225
|
+
* openrouter/x-ai/grok-4.6 low, medium, high, xhigh
|
|
226
|
+
* openrouter/deepseek-v4-pro off, high, xhigh
|
|
227
|
+
*
|
|
228
|
+
* Мало того, ОДНА И ТА ЖЕ модель через подписку и через OpenRouter даёт
|
|
229
|
+
* разные наборы: у `claude-opus-5` в подписке есть `minimal`, а через
|
|
230
|
+
* OpenRouter его нет. Это разные маршруты, и общего списка тут быть не может.
|
|
231
|
+
*
|
|
232
|
+
* Спрашивается только для ВЫБРАННЫХ моделей — их единицы. Обойти так все
|
|
233
|
+
* 366 моделей OpenRouter значило бы держать открытие настроек минутами.
|
|
234
|
+
*/
|
|
235
|
+
async function modelEfforts(llm, provider, model) {
|
|
236
|
+
if (llm === null || llm === undefined || typeof llm.resolveModelInfo !== 'function') return null
|
|
237
|
+
if (typeof provider !== 'string' || provider === '' || typeof model !== 'string' || model === '') return null
|
|
238
|
+
try {
|
|
239
|
+
const info = await llm.resolveModelInfo(provider, model)
|
|
240
|
+
const efforts = info?.reasoning?.efforts
|
|
241
|
+
if (!Array.isArray(efforts) || efforts.length === 0) return null
|
|
242
|
+
return {
|
|
243
|
+
efforts: efforts
|
|
244
|
+
.map((entry) => (typeof entry === 'string' ? entry : String(entry?.id ?? entry?.name ?? '')))
|
|
245
|
+
.filter((id) => id !== ''),
|
|
246
|
+
defaultEffort: info.reasoning.defaultEffort === undefined ? null : String(info.reasoning.defaultEffort),
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// Модель могла исчезнуть или провайдер не ответить — покажем без списка,
|
|
250
|
+
// и человек впишет своё значение вручную.
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
218
255
|
/**
|
|
219
256
|
* Собрать модели, доступные на маршруте.
|
|
220
257
|
*
|
|
@@ -248,6 +285,22 @@ async function listRouteModels(llm, credentials) {
|
|
|
248
285
|
// работает, но записи не имеет, настроен извне: у OpenRouter ключ лежит
|
|
249
286
|
// переменной окружения, и по записям он выглядел бы ненастроенным, хотя
|
|
250
287
|
// отвечает. Проверено на живых данных.
|
|
288
|
+
//
|
|
289
|
+
// «НЕ ЗНАЮ» — ОТДЕЛЬНЫЙ ОТВЕТ, А НЕ «ПО ТОКЕНАМ».
|
|
290
|
+
//
|
|
291
|
+
// Здесь стояла дыра, которая врала в самую дорогую сторону. Когда служба
|
|
292
|
+
// хранилища недоступна, прежний код помечал ВСЕ модели как платные —
|
|
293
|
+
// включая подписочные. Человек видел «по токенам» на claude-opus-5,
|
|
294
|
+
// который ему давно оплачен, и мог отказаться от ревизора из-за счёта,
|
|
295
|
+
// которого не будет.
|
|
296
|
+
//
|
|
297
|
+
// Случай не выдуманный: он воспроизвёлся сразу. Обработчики
|
|
298
|
+
// перечитываются на лету, а оболочка — только при старте, и до
|
|
299
|
+
// перезапуска новый код спрашивал хранилище у старой оболочки, которая
|
|
300
|
+
// его не передавала. Все 388 моделей стали «по токенам».
|
|
301
|
+
//
|
|
302
|
+
// Теперь неизвестность возвращается как `null`, и показ обязан молчать.
|
|
303
|
+
let known = false
|
|
251
304
|
const billing = new Map()
|
|
252
305
|
if (credentials !== undefined && credentials !== null) {
|
|
253
306
|
try {
|
|
@@ -257,8 +310,9 @@ async function listRouteModels(llm, credentials) {
|
|
|
257
310
|
const name = slash >= 0 ? key.slice(slash + 1) : key
|
|
258
311
|
billing.set(name, record.kind === 'grant' ? 'subscription' : 'api-key')
|
|
259
312
|
}
|
|
313
|
+
known = true
|
|
260
314
|
} catch {
|
|
261
|
-
// Хранилище
|
|
315
|
+
// Хранилище не ответило — покажем модели без отметки, это лучше лжи.
|
|
262
316
|
}
|
|
263
317
|
}
|
|
264
318
|
|
|
@@ -266,7 +320,10 @@ async function listRouteModels(llm, credentials) {
|
|
|
266
320
|
for (const info of providers) {
|
|
267
321
|
const name = typeof info === 'string' ? info : info?.name ?? info?.provider
|
|
268
322
|
if (typeof name !== 'string' || name === '') continue
|
|
269
|
-
|
|
323
|
+
// Провайдер без записи, но отвечающий, настроен вне dsh — ключом из
|
|
324
|
+
// окружения. Для человека это значит то же, что сохранённый ключ: счёт
|
|
325
|
+
// придёт. А вот когда хранилище вовсе не спросили, сказать нечего.
|
|
326
|
+
const kind = known ? billing.get(name) ?? 'api-key' : null
|
|
270
327
|
try {
|
|
271
328
|
const models = await llm.listModels(name)
|
|
272
329
|
for (const model of models ?? []) {
|
|
@@ -309,12 +366,40 @@ export async function handleReview(req, res, loadModule, services) {
|
|
|
309
366
|
label: spec.label,
|
|
310
367
|
internal: spec.kind === 'internal',
|
|
311
368
|
optional: spec.optional === true,
|
|
312
|
-
|
|
313
|
-
|
|
369
|
+
// У ВНЕШНИХ ПРОДУКТОВ ОТМЕТКИ ОПЛАТЫ НЕТ, и это не упущение.
|
|
370
|
+
//
|
|
371
|
+
// Claude Code и Codex работают на СВОИХ подписках и нашего маршрута не
|
|
372
|
+
// касаются — мы не знаем и не можем знать, во что обойдётся их запуск.
|
|
373
|
+
// Прежде им доставалась метка «по токенам» просто потому, что она была
|
|
374
|
+
// всем, кроме подписки. Это было неверно.
|
|
375
|
+
models: spec.kind === 'internal' ? routeModels : spec.models.map((id) => ({ provider: null, id, billing: null })),
|
|
376
|
+
// Для ВНЕШНИХ продуктов список остаётся зашитым: спросить у Claude Code
|
|
377
|
+
// или Codex их уровни нечем, они не наш маршрут.
|
|
378
|
+
efforts: spec.kind === 'internal' ? [] : spec.efforts,
|
|
314
379
|
defaultProvider: spec.defaultProvider ?? null,
|
|
315
380
|
defaultModel: spec.defaultModel ?? null,
|
|
316
381
|
}))
|
|
317
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Уровни усилия для тех моделей, что УЖЕ выбраны.
|
|
385
|
+
*
|
|
386
|
+
* Их единицы, и спросить их дёшево. Обходить так все 366 моделей
|
|
387
|
+
* OpenRouter значило бы держать открытие настроек минутами.
|
|
388
|
+
*/
|
|
389
|
+
const effortsFor = async (chosen) => {
|
|
390
|
+
const map = {}
|
|
391
|
+
const seen = new Set()
|
|
392
|
+
for (const entry of chosen.reviewers ?? []) {
|
|
393
|
+
if (entry.provider === null || entry.model === null) continue
|
|
394
|
+
const key = `${entry.provider}/${entry.model}`
|
|
395
|
+
if (seen.has(key)) continue
|
|
396
|
+
seen.add(key)
|
|
397
|
+
const info = await modelEfforts(services?.llm, entry.provider, entry.model)
|
|
398
|
+
if (info !== null) map[key] = info
|
|
399
|
+
}
|
|
400
|
+
return map
|
|
401
|
+
}
|
|
402
|
+
|
|
318
403
|
// Журнал отдаётся вместе с настройками: панель находок и настройки —
|
|
319
404
|
// один раздел, и второй запрос за тем же экраном был бы лишней задержкой.
|
|
320
405
|
const log = await loadModule('./review-log.js')
|
|
@@ -331,7 +416,8 @@ export async function handleReview(req, res, loadModule, services) {
|
|
|
331
416
|
}
|
|
332
417
|
|
|
333
418
|
if (req.method === 'GET') {
|
|
334
|
-
|
|
419
|
+
const current = await settings.readSettings(null)
|
|
420
|
+
reply(res, 200, { catalogue, settings: current, efforts: await effortsFor(current), journal: digest })
|
|
335
421
|
return
|
|
336
422
|
}
|
|
337
423
|
|
|
@@ -367,9 +453,11 @@ export async function handleReview(req, res, loadModule, services) {
|
|
|
367
453
|
'human',
|
|
368
454
|
)
|
|
369
455
|
const after = await log.read()
|
|
456
|
+
const current = await settings.readSettings(null)
|
|
370
457
|
reply(res, 200, {
|
|
371
458
|
catalogue,
|
|
372
|
-
settings:
|
|
459
|
+
settings: current,
|
|
460
|
+
efforts: await effortsFor(current),
|
|
373
461
|
journal: { latest: log.latest(after), accuracy: log.accuracy(after), runs: after.runs.length },
|
|
374
462
|
// Отклонённое возражение должно быть ВИДНО: иначе человек считает,
|
|
375
463
|
// что высказался, а в панели ничего не изменилось.
|
|
@@ -381,7 +469,8 @@ export async function handleReview(req, res, loadModule, services) {
|
|
|
381
469
|
// Список приходит целиком и целиком же замещает сохранённый. Для списка
|
|
382
470
|
// это единственно верно: слияние возвращало бы удалённую запись обратно,
|
|
383
471
|
// то есть удалить ревизора стало бы нельзя.
|
|
384
|
-
|
|
472
|
+
const saved = await settings.writeSettings(null, body)
|
|
473
|
+
reply(res, 200, { catalogue, settings: saved, efforts: await effortsFor(saved), journal: digest })
|
|
385
474
|
} catch (error) {
|
|
386
475
|
reply(res, 500, { problem: String(error?.message ?? error) })
|
|
387
476
|
}
|
package/lib/reviewers.js
CHANGED
|
@@ -63,7 +63,12 @@ export const REVIEWERS = {
|
|
|
63
63
|
label: 'Наш ревизор',
|
|
64
64
|
kind: 'internal',
|
|
65
65
|
models: [],
|
|
66
|
-
|
|
66
|
+
// Уровни усилия НЕ перечисляются: у каждой модели они свои, и зашитый
|
|
67
|
+
// список врал почти везде. Проверено на живых данных: у claude-opus-5
|
|
68
|
+
// их шесть, у deepseek-v4-pro — три, а одна и та же модель через
|
|
69
|
+
// подписку и через OpenRouter даёт РАЗНЫЕ наборы. Их спрашивают у
|
|
70
|
+
// маршрута по выбранной модели.
|
|
71
|
+
efforts: [],
|
|
67
72
|
defaultProvider: 'anthropic',
|
|
68
73
|
defaultModel: 'claude-opus-5',
|
|
69
74
|
},
|
|
@@ -71,7 +76,7 @@ export const REVIEWERS = {
|
|
|
71
76
|
label: 'Наш ревизор, вторая модель',
|
|
72
77
|
kind: 'internal',
|
|
73
78
|
models: [],
|
|
74
|
-
efforts: [
|
|
79
|
+
efforts: [],
|
|
75
80
|
// Намеренно ДРУГОЕ семейство: два прогона одной модели дадут почти
|
|
76
81
|
// одинаковые находки, и второй ревизор не окупит своего времени.
|
|
77
82
|
defaultProvider: 'openai-codex',
|
package/package.json
CHANGED