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.
- package/lib/client.js +203 -110
- package/lib/http-runtime.js +103 -22
- package/lib/index.js +4 -2
- package/lib/review-settings.js +156 -224
- package/lib/review.js +36 -26
- package/lib/reviewers.js +7 -2
- package/package.json +1 -1
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
|
*
|
|
@@ -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
|
-
|
|
285
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
//
|
|
361
|
-
//
|
|
362
|
-
const
|
|
363
|
-
|
|
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
|
})
|