@tzyc/dsh-model-visibility 1.9.1 → 1.10.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/README.md +50 -30
- package/client-src/catalog-store.mjs +464 -0
- package/client-src/checker-view.mjs +221 -0
- package/client-src/composer-filter.mjs +153 -0
- package/client-src/constants.mjs +20 -0
- package/client-src/css.mjs +248 -0
- package/client-src/dict.mjs +427 -0
- package/client-src/format.mjs +154 -0
- package/client-src/hooks.mjs +25 -0
- package/client-src/index.mjs +101 -0
- package/client-src/override-editor.mjs +286 -0
- package/client-src/pure.mjs +136 -0
- package/client-src/settings-section.mjs +582 -0
- package/client-src/tags.mjs +197 -0
- package/client-src/usage-view.mjs +112 -0
- package/client.js +3229 -2258
- package/index.js +178 -0
- package/lib/capability-catalog.js +5 -69
- package/lib/capability-corrections.js +1 -1
- package/lib/check-catalog.js +575 -0
- package/lib/export-csv.js +69 -0
- package/lib/learned-catalog.js +294 -0
- package/lib/model-id.js +50 -0
- package/lib/model-info.js +94 -52
- package/lib/model-overrides.js +1 -1
- package/lib/usage-ledger.js +574 -0
- package/package.json +65 -64
- package/lib/model-catalog.js +0 -670
package/index.js
CHANGED
|
@@ -27,7 +27,11 @@ import path from 'node:path'
|
|
|
27
27
|
import z from '@deepseek-ai/schemastery'
|
|
28
28
|
|
|
29
29
|
import { createModelInfoService } from './lib/model-info.js'
|
|
30
|
+
import { createLearnedCatalog } from './lib/learned-catalog.js'
|
|
30
31
|
import { createOverrideStore } from './lib/model-overrides.js'
|
|
32
|
+
import { createCapabilityVerifier } from './lib/check-catalog.js'
|
|
33
|
+
import { toModelListCsv, toUsageCsv } from './lib/export-csv.js'
|
|
34
|
+
import { createUsageLedger } from './lib/usage-ledger.js'
|
|
31
35
|
import { createProviderProbe, readProviderProfiles } from './lib/provider-probe.js'
|
|
32
36
|
import { stripWrittenFields, writeCapabilities } from './lib/provider-write.js'
|
|
33
37
|
import { installImageInputGate } from './lib/image-input-gate.js'
|
|
@@ -111,6 +115,16 @@ export function apply(ctx, config) {
|
|
|
111
115
|
// needs a synchronous read. See `lib/model-overrides.js`.
|
|
112
116
|
const overrides = createOverrideStore({ logger })
|
|
113
117
|
|
|
118
|
+
// The learned capability snapshot replaces the abandoned static catalog:
|
|
119
|
+
// the online verification run seeds it, and every same-named model served
|
|
120
|
+
// later resolves against the cached rows. See `lib/learned-catalog.js`.
|
|
121
|
+
// Tests may inject a store through the inject scope (`scope.get('learned')`).
|
|
122
|
+
let learned = null
|
|
123
|
+
const learnedFor = () => {
|
|
124
|
+
if (learned !== null) return learned
|
|
125
|
+
learned = createLearnedCatalog({ logger })
|
|
126
|
+
return learned
|
|
127
|
+
}
|
|
114
128
|
/** Current hidden keys, read through the stable volatile reference. */
|
|
115
129
|
const liveList = () => {
|
|
116
130
|
const value = config?.[FIELD]
|
|
@@ -237,15 +251,20 @@ export function apply(ctx, config) {
|
|
|
237
251
|
fallbackProfiles = null
|
|
238
252
|
}
|
|
239
253
|
|
|
254
|
+
const learnedService =
|
|
255
|
+
(typeof scope.get === 'function' ? scope.get('learned') : undefined) ?? learnedFor()
|
|
256
|
+
|
|
240
257
|
const service = createModelInfoService(scope.llm, {
|
|
241
258
|
enrichment: probe === null ? undefined : ({ fresh }) => probe.read({ fresh }),
|
|
242
259
|
overrides,
|
|
260
|
+
learned: learnedService,
|
|
243
261
|
// Synchronous evidence for the request-side image gate: provider profiles
|
|
244
262
|
// from the settings document plus the last completed relay probe.
|
|
245
263
|
context: () => ({
|
|
246
264
|
profiles: probe === null ? profilesForGate() : probe.profiles(),
|
|
247
265
|
discovered: probe?.peek()?.discovered,
|
|
248
266
|
overrides,
|
|
267
|
+
learned: learnedService,
|
|
249
268
|
}),
|
|
250
269
|
})
|
|
251
270
|
|
|
@@ -264,6 +283,36 @@ export function apply(ctx, config) {
|
|
|
264
283
|
})
|
|
265
284
|
scope.effect(() => () => imageInput.dispose(), 'model-visibility: image input gate')
|
|
266
285
|
|
|
286
|
+
// Token-usage ledger behind the page's stats strip: session logs are read
|
|
287
|
+
// incrementally in a background pass, the check sweep records its own
|
|
288
|
+
// spend into the same buckets.
|
|
289
|
+
const usageLedger = createUsageLedger({ logger })
|
|
290
|
+
let lastRescanAt = -Infinity
|
|
291
|
+
const nowMs = () => Date.now()
|
|
292
|
+
const maybeRescan = () => {
|
|
293
|
+
if (usageLedger.isScanning()) return
|
|
294
|
+
if (nowMs() - lastRescanAt < 60_000) return
|
|
295
|
+
lastRescanAt = nowMs()
|
|
296
|
+
usageLedger.rescan().catch(() => {})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// The online capability verifier: the user's chosen checker model plus
|
|
300
|
+
// two public catalogs fill the learned snapshot and (optionally) write
|
|
301
|
+
// back into the provider settings documents.
|
|
302
|
+
const verifier = createCapabilityVerifier({
|
|
303
|
+
llm: scope.llm,
|
|
304
|
+
settings,
|
|
305
|
+
learned: learnedService,
|
|
306
|
+
ledger: usageLedger,
|
|
307
|
+
logger,
|
|
308
|
+
onApplied: () => {
|
|
309
|
+
clearFallbackProfiles()
|
|
310
|
+
probe?.noteSettingsChanged()
|
|
311
|
+
service.invalidate()
|
|
312
|
+
imageInput.refresh()
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
|
|
267
316
|
/** Answer with JSON; the route is diagnostics-friendly on purpose. */
|
|
268
317
|
const json = (value, status = 200) =>
|
|
269
318
|
new Response(JSON.stringify(value), {
|
|
@@ -300,6 +349,7 @@ export function apply(ctx, config) {
|
|
|
300
349
|
probe?.invalidate()
|
|
301
350
|
clearFallbackProfiles()
|
|
302
351
|
overrides.invalidate()
|
|
352
|
+
learnedService.invalidate()
|
|
303
353
|
imageInput.refresh()
|
|
304
354
|
}
|
|
305
355
|
try {
|
|
@@ -434,6 +484,134 @@ export function apply(ctx, config) {
|
|
|
434
484
|
'model-visibility: override route',
|
|
435
485
|
)
|
|
436
486
|
|
|
487
|
+
// Token-usage read for the page's stats strip; the background scan runs
|
|
488
|
+
// whenever the ledger could have grown and the page asks for a read.
|
|
489
|
+
scope.effect(
|
|
490
|
+
() =>
|
|
491
|
+
connection.fetch.register({
|
|
492
|
+
path: `${API}/usage`,
|
|
493
|
+
methods: ['GET', 'POST'],
|
|
494
|
+
requestBody: 'buffered',
|
|
495
|
+
async fetch(request) {
|
|
496
|
+
if (String(request.method || 'GET').toUpperCase() === 'POST') {
|
|
497
|
+
lastRescanAt = -Infinity
|
|
498
|
+
}
|
|
499
|
+
maybeRescan()
|
|
500
|
+
try {
|
|
501
|
+
return json({ ok: true, value: usageLedger.read() })
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return json(
|
|
504
|
+
{
|
|
505
|
+
ok: false,
|
|
506
|
+
error: {
|
|
507
|
+
code: 'usage/failed',
|
|
508
|
+
message: error instanceof Error ? error.message : String(error),
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
500,
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
}),
|
|
516
|
+
'model-visibility: usage route',
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
// One-click exports: the token ledger and the visible-model capability
|
|
520
|
+
// list, both as CSV downloads through the same-origin route.
|
|
521
|
+
scope.effect(
|
|
522
|
+
() =>
|
|
523
|
+
connection.fetch.register({
|
|
524
|
+
path: `${API}/export`,
|
|
525
|
+
methods: ['GET'],
|
|
526
|
+
requestBody: 'buffered',
|
|
527
|
+
async fetch(request) {
|
|
528
|
+
const params = new URL(request.url).searchParams
|
|
529
|
+
const which = params.get('type') === 'models' ? 'models' : 'usage'
|
|
530
|
+
let body
|
|
531
|
+
let name
|
|
532
|
+
try {
|
|
533
|
+
if (which === 'usage') {
|
|
534
|
+
body = toUsageCsv(usageLedger.read())
|
|
535
|
+
name = `model-visibility-usage-${new Date().toISOString().slice(0, 10)}.csv`
|
|
536
|
+
} else {
|
|
537
|
+
const catalog = await service.read()
|
|
538
|
+
body = toModelListCsv(catalog, { hidden: liveList() })
|
|
539
|
+
name = `model-visibility-models-${new Date().toISOString().slice(0, 10)}.csv`
|
|
540
|
+
}
|
|
541
|
+
} catch (error) {
|
|
542
|
+
return json(
|
|
543
|
+
{
|
|
544
|
+
ok: false,
|
|
545
|
+
error: {
|
|
546
|
+
code: 'export/failed',
|
|
547
|
+
message: error instanceof Error ? error.message : String(error),
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
500,
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
return new Response(body, {
|
|
554
|
+
status: 200,
|
|
555
|
+
headers: {
|
|
556
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
557
|
+
'Content-Disposition': `attachment; filename="${name}"`,
|
|
558
|
+
'Cache-Control': 'no-store',
|
|
559
|
+
},
|
|
560
|
+
})
|
|
561
|
+
},
|
|
562
|
+
}),
|
|
563
|
+
'model-visibility: export route',
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
// The verify sweep: state reads are instant, a start posts and the Host
|
|
567
|
+
// runs batches in the background without holding the HTTP call open.
|
|
568
|
+
scope.effect(
|
|
569
|
+
() =>
|
|
570
|
+
connection.fetch.register({
|
|
571
|
+
path: `${API}/verify`,
|
|
572
|
+
methods: ['GET', 'POST'],
|
|
573
|
+
requestBody: 'buffered',
|
|
574
|
+
async fetch(request) {
|
|
575
|
+
const method = String(request.method || 'GET').toUpperCase()
|
|
576
|
+
if (method === 'GET') {
|
|
577
|
+
return json({ ok: true, value: { state: verifier.snapshot(), learned: learnedService.meta() } })
|
|
578
|
+
}
|
|
579
|
+
let body
|
|
580
|
+
try {
|
|
581
|
+
body = await request.json()
|
|
582
|
+
} catch (_error) {
|
|
583
|
+
body = {}
|
|
584
|
+
}
|
|
585
|
+
if (body !== null && typeof body === 'object' && body.cancel === true) {
|
|
586
|
+
verifier.cancel()
|
|
587
|
+
return json({ ok: true, value: { state: verifier.snapshot() } })
|
|
588
|
+
}
|
|
589
|
+
const provider = typeof body?.provider === 'string' ? body.provider : undefined
|
|
590
|
+
const model = typeof body?.model === 'string' ? body.model : undefined
|
|
591
|
+
if (provider === undefined || model === undefined) {
|
|
592
|
+
return json(
|
|
593
|
+
{ ok: false, error: { code: 'verify/params', message: 'provider and model are required' } },
|
|
594
|
+
400,
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
// The sweep runs in the background: the response only reports that
|
|
598
|
+
// it was accepted, and polling GET observes the progress state.
|
|
599
|
+
if (!verifier.isRunning()) {
|
|
600
|
+
verifier
|
|
601
|
+
.start({ checker: { provider, model }, writeBack: body?.writeBack !== false })
|
|
602
|
+
.then(() => {
|
|
603
|
+
// Learned facts (and possibly provider settings) changed.
|
|
604
|
+
service.invalidate()
|
|
605
|
+
imageInput.refresh()
|
|
606
|
+
})
|
|
607
|
+
.catch(() => {})
|
|
608
|
+
}
|
|
609
|
+
return json({ ok: true, value: { state: verifier.snapshot(), accepted: true } })
|
|
610
|
+
},
|
|
611
|
+
}),
|
|
612
|
+
'model-visibility: verify route',
|
|
613
|
+
)
|
|
614
|
+
|
|
437
615
|
// A route/credential change invalidates both the capability cache and (when
|
|
438
616
|
// it actually changed routing facts) the relay-probe cache. Ordinary
|
|
439
617
|
// settings writes — hiding one model — only invalidate the capability
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Model-kind matching and family heuristics.
|
|
3
3
|
*
|
|
4
4
|
* The adapter contract only carries `inputModalities`, and for a custom relay
|
|
5
5
|
* that field is usually the provider's `defaultInput` (text) rather than a fact
|
|
6
|
-
* about the model.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 2. conservative name rules for model families whose capabilities are common
|
|
11
|
-
* knowledge (Claude, GPT-4o+, Gemini, Grok 4+, VL/vision variants).
|
|
6
|
+
* about the model. When neither the adapter nor a verified learned fact knows
|
|
7
|
+
* better, this module answers from conservative name rules for model families
|
|
8
|
+
* whose capabilities are common knowledge (Claude, GPT-4o+, Gemini, Grok 4+,
|
|
9
|
+
* VL/vision variants).
|
|
12
10
|
*
|
|
13
11
|
* Everything returned here is *inferred*: callers must label it as such so the
|
|
14
12
|
* page never presents a guess as an adapter declaration.
|
|
@@ -16,10 +14,6 @@
|
|
|
16
14
|
* @module lib/capability-catalog
|
|
17
15
|
*/
|
|
18
16
|
|
|
19
|
-
import { CATALOG_GENERATED_AT, CATALOG_PI_AI_VERSION, MODEL_CATALOG } from './model-catalog.js'
|
|
20
|
-
|
|
21
|
-
export { CATALOG_GENERATED_AT, CATALOG_PI_AI_VERSION, MODEL_CATALOG }
|
|
22
|
-
|
|
23
17
|
/** Kinds the page knows how to label. */
|
|
24
18
|
export const MODEL_KINDS = Object.freeze([
|
|
25
19
|
'chat',
|
|
@@ -42,64 +36,6 @@ const TRANSLATION_WORDS = /(^|[-_./\s])(translate|translation|translator)([-_./\
|
|
|
42
36
|
|
|
43
37
|
const VISION_WORDS = /(^|[-_./\s])(vl|vision|visual|omni|multimodal|mm)([-_./\s]|$)|llava|pixtral|internvl|molmo|idefics|qwen[\d.]*-?vl|glm-[\d.]+v|kimi-.*-vl/
|
|
44
38
|
|
|
45
|
-
/** Strip aggregator namespaces, request suffixes and resolution variants. */
|
|
46
|
-
export function normalizeModelId(value) {
|
|
47
|
-
let id = String(value ?? '').trim().toLowerCase()
|
|
48
|
-
if (id === '') return ''
|
|
49
|
-
id = id.replace(/^~/, '')
|
|
50
|
-
// Providers may stack request variants (for example `:free:thinking`).
|
|
51
|
-
// Strip them one at a time so aliases remain stable without broad folding.
|
|
52
|
-
let previous
|
|
53
|
-
do {
|
|
54
|
-
previous = id
|
|
55
|
-
id = id.replace(/:(batch|free|extended|thinking|online|nitro)$/, '')
|
|
56
|
-
id = id.replace(/-(free|batch)$/, '')
|
|
57
|
-
} while (id !== previous)
|
|
58
|
-
const slash = id.lastIndexOf('/')
|
|
59
|
-
if (slash !== -1) id = id.slice(slash + 1)
|
|
60
|
-
id = id.replace(/^(global|us|eu|apac|jp|au|ca)\./, '')
|
|
61
|
-
id = id.replace(/^(anthropic|openai|google|xai|x-ai|meta|mistralai|moonshotai|minimax|nvidia|qwen|z-ai|zai|deepseek|bytedance-seed)\./, '')
|
|
62
|
-
return id
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Lookup keys for one model id, most specific first. */
|
|
66
|
-
export function modelIdVariants(value) {
|
|
67
|
-
const base = normalizeModelId(value)
|
|
68
|
-
if (base === '') return []
|
|
69
|
-
const variants = [base]
|
|
70
|
-
const withoutResolution = base.replace(/-(4k|2k|8k|hd|uhd)$/, '')
|
|
71
|
-
if (withoutResolution !== base) variants.push(withoutResolution)
|
|
72
|
-
const withoutSuffix = withoutResolution.replace(/-(latest|preview|new)$/, '')
|
|
73
|
-
if (!variants.includes(withoutSuffix)) variants.push(withoutSuffix)
|
|
74
|
-
const compact = base.replace(/[^a-z0-9]+/g, '')
|
|
75
|
-
if (compact !== '' && !variants.includes(compact)) variants.push(compact)
|
|
76
|
-
return variants
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Exact/alias index over the generated snapshot. */
|
|
80
|
-
const INDEX = (() => {
|
|
81
|
-
const index = new Map()
|
|
82
|
-
for (const [id, entry] of Object.entries(MODEL_CATALOG)) {
|
|
83
|
-
for (const variant of modelIdVariants(id)) {
|
|
84
|
-
if (!index.has(variant)) index.set(variant, { id, ...entry })
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return index
|
|
88
|
-
})()
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Find the snapshot entry for one model id.
|
|
92
|
-
* @param value - model id as any provider spells it.
|
|
93
|
-
* @returns `{ id, type, input?, output?, reasoning?, contextWindow?, maxTokens?, provider? }` or undefined.
|
|
94
|
-
*/
|
|
95
|
-
export function lookupCatalogModel(value) {
|
|
96
|
-
for (const variant of modelIdVariants(value)) {
|
|
97
|
-
const found = INDEX.get(variant)
|
|
98
|
-
if (found !== undefined) return found
|
|
99
|
-
}
|
|
100
|
-
return undefined
|
|
101
|
-
}
|
|
102
|
-
|
|
103
39
|
/** Coarse kind from the id/name alone. */
|
|
104
40
|
export function inferModelKind(id, name = '') {
|
|
105
41
|
const text = `${String(id ?? '')} ${String(name ?? '')}`.toLowerCase()
|