tensorgrid-ui 1.10.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.
@@ -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
  *
@@ -225,7 +262,7 @@ export async function handleUpdate(req, res, state, loadModule) {
225
262
  * @param llm - служба моделей; может отсутствовать в профиле без неё.
226
263
  * @returns пары «провайдер и имя модели», пригодные для показа.
227
264
  */
228
- async function listRouteModels(llm) {
265
+ async function listRouteModels(llm, credentials) {
229
266
  if (llm === undefined || llm === null || typeof llm.listProviders !== 'function') return []
230
267
 
231
268
  const providers = (() => {
@@ -236,15 +273,42 @@ async function listRouteModels(llm) {
236
273
  }
237
274
  })()
238
275
 
276
+ // ── ЧЕМ ОПЛАЧЕН КАЖДЫЙ ПРОВАЙДЕР ────────────────────────────────────
277
+ //
278
+ // Это не украшение: подписка безлимитна, а ключ API считается по токенам.
279
+ // Человек, собирающий состав ревизоров, должен видеть разницу ДО того,
280
+ // как получит счёт, — особенно когда ревизоров несколько и каждый читает
281
+ // весь код.
282
+ //
283
+ // Признак берётся из хранилища учётных данных: запись вида `grant` — это
284
+ // вход по подписке, `api-key` — сохранённый ключ. Провайдер, который
285
+ // работает, но записи не имеет, настроен извне: у OpenRouter ключ лежит
286
+ // переменной окружения, и по записям он выглядел бы ненастроенным, хотя
287
+ // отвечает. Проверено на живых данных.
288
+ const billing = new Map()
289
+ if (credentials !== undefined && credentials !== null) {
290
+ try {
291
+ for (const record of await credentials.listRecords()) {
292
+ const key = String(record.key ?? '')
293
+ const slash = key.lastIndexOf('/')
294
+ const name = slash >= 0 ? key.slice(slash + 1) : key
295
+ billing.set(name, record.kind === 'grant' ? 'subscription' : 'api-key')
296
+ }
297
+ } catch {
298
+ // Хранилище недоступно — покажем модели без отметки, это лучше отказа.
299
+ }
300
+ }
301
+
239
302
  const collected = []
240
303
  for (const info of providers) {
241
304
  const name = typeof info === 'string' ? info : info?.name ?? info?.provider
242
305
  if (typeof name !== 'string' || name === '') continue
306
+ const kind = billing.get(name) ?? 'external'
243
307
  try {
244
308
  const models = await llm.listModels(name)
245
309
  for (const model of models ?? []) {
246
310
  const id = typeof model === 'string' ? model : model?.id ?? model?.name
247
- if (typeof id === 'string' && id !== '') collected.push({ provider: name, id })
311
+ if (typeof id === 'string' && id !== '') collected.push({ provider: name, id, billing: kind })
248
312
  }
249
313
  } catch {
250
314
  // Провайдер без списка или с истёкшим ключом просто не добавит своих.
@@ -275,22 +339,40 @@ export async function handleReview(req, res, loadModule, services) {
275
339
  // Для внешних продуктов так не выйдет, и это не лень: Claude Code и
276
340
  // Codex понимают только СВОИ имена моделей, а спросить у них список
277
341
  // нечем. Там короткий перечень плюс возможность вписать своё.
278
- const routeModels = await listRouteModels(services?.llm)
342
+ const routeModels = await listRouteModels(services?.llm, services?.credentials)
279
343
 
280
344
  const catalogue = Object.entries(REVIEWERS).map(([kind, spec]) => ({
281
345
  kind,
282
346
  label: spec.label,
283
347
  internal: spec.kind === 'internal',
284
- models: spec.kind === 'internal' ? routeModels : spec.models.map((id) => ({ provider: null, id })),
285
- efforts: spec.efforts,
348
+ optional: spec.optional === true,
349
+ models: spec.kind === 'internal' ? routeModels : spec.models.map((id) => ({ provider: null, id, billing: 'external' })),
350
+ // Для ВНЕШНИХ продуктов список остаётся зашитым: спросить у Claude Code
351
+ // или Codex их уровни нечем, они не наш маршрут.
352
+ efforts: spec.kind === 'internal' ? [] : spec.efforts,
353
+ defaultProvider: spec.defaultProvider ?? null,
354
+ defaultModel: spec.defaultModel ?? null,
286
355
  }))
287
356
 
288
- const modes = Object.entries(settings.MODES).map(([id, spec]) => ({
289
- id,
290
- label: spec.label,
291
- hint: spec.hint,
292
- enabled: spec.enabled,
293
- }))
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
+ }
294
376
 
295
377
  // Журнал отдаётся вместе с настройками: панель находок и настройки —
296
378
  // один раздел, и второй запрос за тем же экраном был бы лишней задержкой.
@@ -308,7 +390,8 @@ export async function handleReview(req, res, loadModule, services) {
308
390
  }
309
391
 
310
392
  if (req.method === 'GET') {
311
- reply(res, 200, { catalogue, modes, 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 })
312
395
  return
313
396
  }
314
397
 
@@ -344,10 +427,11 @@ export async function handleReview(req, res, loadModule, services) {
344
427
  'human',
345
428
  )
346
429
  const after = await log.read()
430
+ const current = await settings.readSettings(null)
347
431
  reply(res, 200, {
348
432
  catalogue,
349
- modes,
350
- settings: await settings.readSettings(null),
433
+ settings: current,
434
+ efforts: await effortsFor(current),
351
435
  journal: { latest: log.latest(after), accuracy: log.accuracy(after), runs: after.runs.length },
352
436
  // Отклонённое возражение должно быть ВИДНО: иначе человек считает,
353
437
  // что высказался, а в панели ничего не изменилось.
@@ -356,14 +440,11 @@ export async function handleReview(req, res, loadModule, services) {
356
440
  return
357
441
  }
358
442
 
359
- // Переключение режима и поштучная правка приходят одним путём: если в
360
- // теле есть `mode`, состав считает Host по режиму, а не браузер. Так
361
- // определение режима живёт в одном месте, а не в двух.
362
- const next = settings.isMode(body.mode) && body.applyMode === true
363
- ? settings.applyMode(await settings.readSettings(null), body.mode)
364
- : body
365
-
366
- reply(res, 200, { catalogue, modes, settings: await settings.writeSettings(null, next), journal: digest })
443
+ // Список приходит целиком и целиком же замещает сохранённый. Для списка
444
+ // это единственно верно: слияние возвращало бы удалённую запись обратно,
445
+ // то есть удалить ревизора стало бы нельзя.
446
+ const saved = await settings.writeSettings(null, body)
447
+ reply(res, 200, { catalogue, settings: saved, efforts: await effortsFor(saved), journal: digest })
367
448
  } catch (error) {
368
449
  reply(res, 500, { problem: String(error?.message ?? error) })
369
450
  }
package/lib/index.js CHANGED
@@ -224,14 +224,16 @@ export function apply(ctx) {
224
224
  // подключённых моделей для выбора ревизора. Отдельным — потому что
225
225
  // объединение с парой выше связало бы настройки обзора со стойкой
226
226
  // входа: нет одной — пропала бы и другая, хотя они независимы.
227
- const modelServices = { llm: null, generation: null }
228
- web.inject(['llm'], (bound) => {
227
+ const modelServices = { llm: null, credentials: null, generation: null }
228
+ web.inject(['llm', 'credentials'], (bound) => {
229
229
  const generation = {}
230
230
  modelServices.llm = bound.llm
231
+ modelServices.credentials = bound.credentials
231
232
  modelServices.generation = generation
232
233
  bound.effect(() => () => {
233
234
  if (modelServices.generation !== generation) return
234
235
  modelServices.llm = null
236
+ modelServices.credentials = null
235
237
  modelServices.generation = null
236
238
  })
237
239
  })