tensorgrid-ui 2.0.0 → 2.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/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
- * у Claude Code или Codex их собственный перечень нечем. Поэтому рядом со
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
- ...options.map((option) => jsx('option', { value: option.value, children: option.label }, option.value)),
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,10 +2518,16 @@ window.__ModuleLoader__.load({
2487
2518
  }])
2488
2519
  }
2489
2520
 
2521
+ /**
2522
+ * Отметка на карточке: по подписке или за отдельные деньги.
2523
+ *
2524
+ * Ровно два состояния, как и в списке моделей. Разница между
2525
+ * сохранённым ключом и ключом из окружения человека не касается:
2526
+ * и то и другое означает счёт по токенам.
2527
+ */
2490
2528
  const billingLabel = (kind) => {
2491
- if (kind === 'subscription') return t('review.bySubscription')
2492
- if (kind === 'api-key') return t('review.byApiKey')
2493
- return null
2529
+ if (kind === undefined || kind === null) return null
2530
+ return kind === 'subscription' ? t('review.bySubscription') : t('review.byApiKey')
2494
2531
  }
2495
2532
 
2496
2533
  const atLimit = reviewers.length >= 8
@@ -2555,15 +2592,33 @@ window.__ModuleLoader__.load({
2555
2592
  value: entry.model,
2556
2593
  disabled: busy,
2557
2594
  placeholder: t('review.modelName'),
2558
- options: models.map((m) => ({
2559
- value: m.id,
2560
- // Провайдер и способ оплаты стоят в самом
2561
- // названии: у OpenRouter сотни моделей, и без
2562
- // этого выбрать осмысленно нельзя.
2563
- label: (m.provider === null ? '' : m.provider + ' \u00b7 ') + m.id
2564
- + (m.billing === 'subscription' ? ' \u2014 ' + t('review.bySubscription') : '')
2565
- + (m.billing === 'api-key' ? ' \u2014 ' + t('review.byApiKey') : ''),
2566
- })),
2595
+ options: models.map((m) => ({ value: m.id, label: m.id })),
2596
+ // РАЗДЕЛЕНИЕ ПО ОПЛАТЕ.
2597
+ //
2598
+ // Один и тот же claude-opus-5 доступен и по
2599
+ // подписке, и через OpenRouter по токенам. В общем
2600
+ // списке они неразличимы, а разница в том, придёт
2601
+ // ли за них счёт. Провайдер стоит в названии: у
2602
+ // OpenRouter сотни моделей, и без него не понять,
2603
+ // чья она.
2604
+ // Групп ДВЕ, а не три. Различие, которое важно
2605
+ // человеку, одно: придёт ли за эту модель
2606
+ // отдельный счёт. Ключ, сохранённый в dsh, и
2607
+ // ключ из окружения — с этой стороны одно и то
2608
+ // же, а третья группа оказывалась почти пустой и
2609
+ // только мешала выбирать.
2610
+ groups: [
2611
+ {
2612
+ label: t('review.groupSubscription'),
2613
+ options: models.filter((m) => m.billing === 'subscription')
2614
+ .map((m) => ({ value: m.id, label: m.provider + ' \u00b7 ' + m.id })),
2615
+ },
2616
+ {
2617
+ label: t('review.groupPaid'),
2618
+ options: models.filter((m) => m.billing !== 'subscription')
2619
+ .map((m) => ({ value: m.id, label: (m.provider === null ? '' : m.provider + ' \u00b7 ') + m.id })),
2620
+ },
2621
+ ],
2567
2622
  onPick: (value) => {
2568
2623
  const found = models.find((m) => m.id === value)
2569
2624
  patch(entry.id, {
@@ -2583,7 +2638,15 @@ window.__ModuleLoader__.load({
2583
2638
  value: entry.effort,
2584
2639
  disabled: busy,
2585
2640
  placeholder: t('review.effortName'),
2586
- options: (spec && spec.efforts ? spec.efforts : []).map((e) => ({ value: e, label: e })),
2641
+ // Уровни у КАЖДОЙ МОДЕЛИ свои, и зашитый список
2642
+ // врал почти везде. Проверено: у claude-opus-5 их
2643
+ // шесть, у deepseek-v4-pro — три, а одна и та же
2644
+ // модель через подписку и через OpenRouter даёт
2645
+ // разные наборы.
2646
+ //
2647
+ // Пока модель не выбрана, спрашивать нечего:
2648
+ // остаётся ввод своего значения.
2649
+ options: effortsFor(entry).map((e) => ({ value: e, label: e })),
2587
2650
  onPick: (value) => patch(entry.id, { effort: value }),
2588
2651
  }),
2589
2652
  ],
@@ -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
  *
@@ -310,11 +347,33 @@ export async function handleReview(req, res, loadModule, services) {
310
347
  internal: spec.kind === 'internal',
311
348
  optional: spec.optional === true,
312
349
  models: spec.kind === 'internal' ? routeModels : spec.models.map((id) => ({ provider: null, id, billing: 'external' })),
313
- efforts: spec.efforts,
350
+ // Для ВНЕШНИХ продуктов список остаётся зашитым: спросить у Claude Code
351
+ // или Codex их уровни нечем, они не наш маршрут.
352
+ efforts: spec.kind === 'internal' ? [] : spec.efforts,
314
353
  defaultProvider: spec.defaultProvider ?? null,
315
354
  defaultModel: spec.defaultModel ?? null,
316
355
  }))
317
356
 
357
+ /**
358
+ * Уровни усилия для тех моделей, что УЖЕ выбраны.
359
+ *
360
+ * Их единицы, и спросить их дёшево. Обходить так все 366 моделей
361
+ * OpenRouter значило бы держать открытие настроек минутами.
362
+ */
363
+ const effortsFor = async (chosen) => {
364
+ const map = {}
365
+ const seen = new Set()
366
+ for (const entry of chosen.reviewers ?? []) {
367
+ if (entry.provider === null || entry.model === null) continue
368
+ const key = `${entry.provider}/${entry.model}`
369
+ if (seen.has(key)) continue
370
+ seen.add(key)
371
+ const info = await modelEfforts(services?.llm, entry.provider, entry.model)
372
+ if (info !== null) map[key] = info
373
+ }
374
+ return map
375
+ }
376
+
318
377
  // Журнал отдаётся вместе с настройками: панель находок и настройки —
319
378
  // один раздел, и второй запрос за тем же экраном был бы лишней задержкой.
320
379
  const log = await loadModule('./review-log.js')
@@ -331,7 +390,8 @@ export async function handleReview(req, res, loadModule, services) {
331
390
  }
332
391
 
333
392
  if (req.method === 'GET') {
334
- reply(res, 200, { catalogue, settings: await settings.readSettings(null), journal: digest })
393
+ const current = await settings.readSettings(null)
394
+ reply(res, 200, { catalogue, settings: current, efforts: await effortsFor(current), journal: digest })
335
395
  return
336
396
  }
337
397
 
@@ -367,9 +427,11 @@ export async function handleReview(req, res, loadModule, services) {
367
427
  'human',
368
428
  )
369
429
  const after = await log.read()
430
+ const current = await settings.readSettings(null)
370
431
  reply(res, 200, {
371
432
  catalogue,
372
- settings: await settings.readSettings(null),
433
+ settings: current,
434
+ efforts: await effortsFor(current),
373
435
  journal: { latest: log.latest(after), accuracy: log.accuracy(after), runs: after.runs.length },
374
436
  // Отклонённое возражение должно быть ВИДНО: иначе человек считает,
375
437
  // что высказался, а в панели ничего не изменилось.
@@ -381,7 +443,8 @@ export async function handleReview(req, res, loadModule, services) {
381
443
  // Список приходит целиком и целиком же замещает сохранённый. Для списка
382
444
  // это единственно верно: слияние возвращало бы удалённую запись обратно,
383
445
  // то есть удалить ревизора стало бы нельзя.
384
- reply(res, 200, { catalogue, settings: await settings.writeSettings(null, body), journal: digest })
446
+ const saved = await settings.writeSettings(null, body)
447
+ reply(res, 200, { catalogue, settings: saved, efforts: await effortsFor(saved), journal: digest })
385
448
  } catch (error) {
386
449
  reply(res, 500, { problem: String(error?.message ?? error) })
387
450
  }
package/lib/reviewers.js CHANGED
@@ -63,7 +63,12 @@ export const REVIEWERS = {
63
63
  label: 'Наш ревизор',
64
64
  kind: 'internal',
65
65
  models: [],
66
- efforts: ['low', 'medium', 'high', 'max'],
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: ['low', 'medium', 'high', 'max'],
79
+ efforts: [],
75
80
  // Намеренно ДРУГОЕ семейство: два прогона одной модели дадут почти
76
81
  // одинаковые находки, и второй ревизор не окупит своего времени.
77
82
  defaultProvider: 'openai-codex',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tensorgrid-ui",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "TENSOR GRID — фирменный интерфейс поверх DeepSeek Harness: палитра, живой ambient-слой, айдентика и русский язык-пакет",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",