@tzyc/dsh-model-visibility 1.10.2 → 1.12.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 +64 -15
- package/client-src/catalog-store.mjs +95 -18
- package/client-src/checker-view.mjs +158 -65
- package/client-src/css.mjs +153 -221
- package/client-src/dict.mjs +119 -81
- package/client-src/format.mjs +9 -2
- package/client-src/pure.mjs +24 -7
- package/client-src/settings-section.mjs +242 -137
- package/client-src/usage-view.mjs +69 -65
- package/client.js +869 -596
- package/index.js +4 -2
- package/lib/check-catalog.js +868 -108
- package/lib/export-csv.js +48 -14
- package/lib/learned-catalog.js +10 -2
- package/lib/model-info.js +4 -2
- package/lib/model-overrides.js +9 -4
- package/lib/provider-probe.js +138 -22
- package/lib/provider-write.js +31 -7
- package/package.json +2 -2
package/lib/export-csv.js
CHANGED
|
@@ -8,33 +8,61 @@
|
|
|
8
8
|
* @module lib/export-csv
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
/** Quote one CSV field, RFC 4180 style. */
|
|
11
|
+
/** Quote one CSV field, RFC 4180 style, and defuse spreadsheet formulas. */
|
|
12
12
|
function csvField(value) {
|
|
13
13
|
if (value === null || value === undefined) return ''
|
|
14
14
|
let text = String(value)
|
|
15
|
+
// A leading =, +, -, @ or tab makes Excel/Sheets evaluate the cell. Model
|
|
16
|
+
// ids come from a third-party relay, so they are not trusted input.
|
|
17
|
+
if (/^[=+\-@\t\r]/.test(text)) text = `'${text}`
|
|
15
18
|
if (/[",\r\n]/.test(text)) text = `"${text.replace(/"/g, '""')}"`
|
|
16
19
|
return text
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
/**
|
|
22
|
+
/** Render one bucket tuple as four CSV columns. */
|
|
23
|
+
function bucketFields(buckets) {
|
|
24
|
+
return [0, 1, 2, 3].map((index) => csvField(buckets?.[index] ?? 0))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Prefix the sheet with the window totals, then one (day, route) row each.
|
|
29
|
+
*
|
|
30
|
+
* `summary.days` is `{ dayKey: [in, out, cacheRead, cacheWrite] }` — the ledger
|
|
31
|
+
* folds to a single tuple per day. Per-route detail lives in `summary.byModel`
|
|
32
|
+
* (`{ "provider/model": tuple }`) and has no day dimension, so it is exported
|
|
33
|
+
* as its own section rather than being mistaken for a day/route map.
|
|
34
|
+
*/
|
|
20
35
|
export function toUsageCsv(summary) {
|
|
21
|
-
const fmt = (buckets) => `${buckets?.[0] ?? 0},${buckets?.[1] ?? 0},${buckets?.[2] ?? 0},${buckets?.[3] ?? 0}`
|
|
22
36
|
const rows = []
|
|
23
37
|
rows.push('section,range,input,output,cacheRead,cacheWrite')
|
|
24
38
|
const ranges = summary?.ranges ?? {}
|
|
25
39
|
for (const name of ['today', 'week', 'month', 'total']) {
|
|
26
|
-
|
|
40
|
+
// `range` is a single bucket tuple, so it expands to the four numeric
|
|
41
|
+
// columns rather than being joined as if it were a string.
|
|
42
|
+
rows.push(['sum', name, ...bucketFields(ranges[name])].join(','))
|
|
27
43
|
}
|
|
44
|
+
|
|
28
45
|
rows.push('')
|
|
29
|
-
rows.push('day,
|
|
46
|
+
rows.push('day,input,output,cacheRead,cacheWrite')
|
|
30
47
|
const days = summary?.days ?? {}
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
for (const dayKey of Object.keys(days).sort()) {
|
|
49
|
+
const values = days[dayKey]
|
|
50
|
+
if (!Array.isArray(values)) continue
|
|
51
|
+
if (values.every((n) => n === 0)) continue
|
|
52
|
+
rows.push([csvField(dayKey), ...bucketFields(values)].join(','))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const byModel = summary?.byModel ?? {}
|
|
56
|
+
const modelKeys = Object.keys(byModel).sort()
|
|
57
|
+
if (modelKeys.length > 0) {
|
|
58
|
+
rows.push('')
|
|
59
|
+
rows.push('provider/model,input,output,cacheRead,cacheWrite')
|
|
60
|
+
for (const route of modelKeys) {
|
|
61
|
+
if (route === '*') continue
|
|
62
|
+
const values = byModel[route]
|
|
63
|
+
if (!Array.isArray(values)) continue
|
|
64
|
+
if (values.every((n) => n === 0)) continue
|
|
65
|
+
rows.push([csvField(route), ...bucketFields(values)].join(','))
|
|
38
66
|
}
|
|
39
67
|
}
|
|
40
68
|
return `${rows.join('\r\n')}\r\n`
|
|
@@ -47,7 +75,13 @@ export function toModelListCsv(catalog, { hidden = [] } = {}) {
|
|
|
47
75
|
for (const group of catalog?.groups ?? []) {
|
|
48
76
|
for (const model of group?.models ?? []) {
|
|
49
77
|
const sources = model.sources ?? {}
|
|
50
|
-
|
|
78
|
+
// `efforts` is an array of `{ id, name }`; joining it needs the ids, not
|
|
79
|
+
// the array itself (the old code called `.join` on the array and threw).
|
|
80
|
+
const efforts = Array.isArray(model.reasoning?.efforts)
|
|
81
|
+
? model.reasoning.efforts
|
|
82
|
+
.map((effort) => (typeof effort === 'string' ? effort : effort?.id))
|
|
83
|
+
.filter((id) => typeof id === 'string' && id !== '')
|
|
84
|
+
: []
|
|
51
85
|
rows.push(
|
|
52
86
|
[
|
|
53
87
|
group.id,
|
|
@@ -56,7 +90,7 @@ export function toModelListCsv(catalog, { hidden = [] } = {}) {
|
|
|
56
90
|
model.kind ?? '',
|
|
57
91
|
(model.inputModalities ?? []).join('|'),
|
|
58
92
|
model.contextWindow ?? '',
|
|
59
|
-
|
|
93
|
+
efforts.join('|'),
|
|
60
94
|
model.reasoningSupported === true ? 'yes' : model.reasoningSupported === false ? 'no' : '',
|
|
61
95
|
`${sources.modalities ?? 'unknown'};${sources.context ?? 'unknown'};${sources.reasoning ?? 'unknown'}`,
|
|
62
96
|
]
|
package/lib/learned-catalog.js
CHANGED
|
@@ -232,8 +232,15 @@ export function createLearnedCatalog(options = {}) {
|
|
|
232
232
|
* from the checker), so records merge instead of replacing: newer facts
|
|
233
233
|
* overwrite older ones field by field and the freshest `verifiedAt` stays.
|
|
234
234
|
* @param inputs - check results, each `{ id | provider+model, …facts }`.
|
|
235
|
+
* @param extra - provenance for the whole call: `verifiedAt`, `origins`,
|
|
236
|
+
* `checker`. `origins` is merged into each record so a later public-only
|
|
237
|
+
* pass appends `public` next to an earlier `checker` instead of replacing
|
|
238
|
+
* it (which is what made "publicOnly: 0" look like a missing source).
|
|
235
239
|
*/
|
|
236
240
|
const upsertEntries = (inputs, extra = {}) => {
|
|
241
|
+
// A verify sweep now flushes one array of facts per batch instead of one
|
|
242
|
+
// call per model. Reloading by force for every call forced a full file read
|
|
243
|
+
// and a full rewrite per model (199 reads + 199 writes for 200 models).
|
|
237
244
|
load(true)
|
|
238
245
|
const source = Array.isArray(inputs) ? inputs : [inputs]
|
|
239
246
|
let changed = false
|
|
@@ -241,10 +248,11 @@ export function createLearnedCatalog(options = {}) {
|
|
|
241
248
|
const sanitized = sanitizeLearned(raw)
|
|
242
249
|
if (sanitized === undefined) continue
|
|
243
250
|
// Call-side provenance beats the entry's own stamps: the orchestrator
|
|
244
|
-
// knows the run that produced the fact.
|
|
251
|
+
// knows the run that produced the fact. An entry that carries its own
|
|
252
|
+
// `origins` keeps them, so a public-only row stays `['public']`.
|
|
245
253
|
const provenance = {}
|
|
246
254
|
if (extra?.verifiedAt !== undefined) provenance.verifiedAt = extra.verifiedAt
|
|
247
|
-
if (extra?.origins !== undefined) provenance.origins = extra.origins
|
|
255
|
+
if (extra?.origins !== undefined && raw?.origins === undefined) provenance.origins = extra.origins
|
|
248
256
|
if (extra?.checker !== undefined) provenance.checker = extra.checker
|
|
249
257
|
const record = { provider: raw?.provider, ...sanitized.entry, ...provenance, id: sanitized.id }
|
|
250
258
|
const key = modelIdVariants(record.id)[0] ?? sanitized.id
|
package/lib/model-info.js
CHANGED
|
@@ -700,8 +700,10 @@ function catalogMeta() {
|
|
|
700
700
|
// `reload` means the route understands `?reload=1` and drops every cache;
|
|
701
701
|
// `overrides` means the Host persists and serves manual capability edits;
|
|
702
702
|
// `verify` means the Host can sweep every model online; `usage` means the
|
|
703
|
-
// Host serves the token-usage ledger
|
|
704
|
-
|
|
703
|
+
// Host serves the token-usage ledger; `checkerBudget` means the verify
|
|
704
|
+
// sweep sizes its output budget and pins a reasoning effort (a Host
|
|
705
|
+
// without it truncates every batch on a thinking checker).
|
|
706
|
+
features: ['applyInput', 'reload', 'overrides', 'verify', 'usage', 'checkerBudget'],
|
|
705
707
|
}
|
|
706
708
|
}
|
|
707
709
|
|
package/lib/model-overrides.js
CHANGED
|
@@ -30,10 +30,15 @@ import { MODEL_KINDS } from './capability-catalog.js'
|
|
|
30
30
|
export const OVERRIDE_MODALITIES = ['text', 'image', 'audio', 'video', 'file']
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Effort ids
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
* Effort ids the editor offers and the provider schema can key.
|
|
34
|
+
*
|
|
35
|
+
* The list is intentionally wider than the `off/low/medium/high` ladder most
|
|
36
|
+
* adapters publish: relays and vendors also use `minimal`, `max` and `xhigh`,
|
|
37
|
+
* and the whole point of a manual override is to describe the upstream the
|
|
38
|
+
* adapter cannot describe. It happens to be the same set `llm-pi-ai` accepts
|
|
39
|
+
* as `reasoningEfforts` keys, so `lib/provider-write.js` keeps its own frozen
|
|
40
|
+
* copy for the write path (a schema change there must not silently widen the
|
|
41
|
+
* editor, or vice versa).
|
|
37
42
|
*/
|
|
38
43
|
export const OVERRIDE_EFFORT_IDS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
|
39
44
|
|
package/lib/provider-probe.js
CHANGED
|
@@ -231,6 +231,27 @@ export function parseModelListing(body) {
|
|
|
231
231
|
return out
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Build the model-id set from a New API `/api/pricing` response.
|
|
236
|
+
*
|
|
237
|
+
* A relay that refuses or times out on `/models` usually still answers the
|
|
238
|
+
* public pricing endpoint, and a bare id list is enough for the page to show
|
|
239
|
+
* the right total and to classify every model it serves.
|
|
240
|
+
* @param body - parsed `/api/pricing` payload.
|
|
241
|
+
* @returns a Map of model id to `{ id }` (kind filled in by the caller).
|
|
242
|
+
*/
|
|
243
|
+
export function parsePricingIds(body) {
|
|
244
|
+
const out = new Map()
|
|
245
|
+
const data = body !== null && typeof body === 'object' && Array.isArray(body.data) ? body.data : []
|
|
246
|
+
for (const raw of data) {
|
|
247
|
+
if (raw === null || typeof raw !== 'object') continue
|
|
248
|
+
const id = str(raw.model_name) ?? str(raw.model) ?? str(raw.modelName) ?? str(raw.id)
|
|
249
|
+
if (id === undefined) continue
|
|
250
|
+
out.set(id, { id })
|
|
251
|
+
}
|
|
252
|
+
return out
|
|
253
|
+
}
|
|
254
|
+
|
|
234
255
|
/**
|
|
235
256
|
* Parse New API's `/api/pricing` response (model kind only).
|
|
236
257
|
* @param body - parsed JSON.
|
|
@@ -321,11 +342,46 @@ export function profileSignature(profiles) {
|
|
|
321
342
|
export function listingUrls(baseURL) {
|
|
322
343
|
const base = String(baseURL).replace(/\/+$/, '')
|
|
323
344
|
if (!/^https?:\/\//i.test(base)) return []
|
|
345
|
+
// A plaintext base URL would put the resolved API key on the wire in clear
|
|
346
|
+
// text. Loopback stays allowed for a local gateway.
|
|
347
|
+
if (!isSecureBase(base) && !isLoopback(base)) return []
|
|
324
348
|
const urls = [`${base}/models`]
|
|
325
349
|
if (!/\/v\d+$/i.test(base)) urls.push(`${base}/v1/models`)
|
|
326
350
|
return urls
|
|
327
351
|
}
|
|
328
352
|
|
|
353
|
+
/** True when the base URL speaks TLS. */
|
|
354
|
+
function isSecureBase(value) {
|
|
355
|
+
return /^https:\/\//i.test(String(value))
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** True for a loopback host, where plaintext never leaves the machine. */
|
|
359
|
+
function isLoopback(value) {
|
|
360
|
+
try {
|
|
361
|
+
const host = new URL(value).hostname.toLowerCase()
|
|
362
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'
|
|
363
|
+
} catch (_error) {
|
|
364
|
+
return false
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Turn a transport/DNS failure into something the page can show. */
|
|
369
|
+
function transportMessage(error) {
|
|
370
|
+
const code = error?.code ?? error?.cause?.code
|
|
371
|
+
if (typeof code === 'string') {
|
|
372
|
+
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN') return '域名解析失败(DNS 查不到该主机)'
|
|
373
|
+
if (code === 'ECONNREFUSED') return '连接被拒绝'
|
|
374
|
+
if (code === 'ECONNRESET') return '连接被重置'
|
|
375
|
+
if (code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT') return '连接超时'
|
|
376
|
+
if (code === 'CERT_HAS_EXPIRED') return 'TLS 证书已过期'
|
|
377
|
+
if (code.startsWith('ERR_TLS') || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') return 'TLS 校验失败'
|
|
378
|
+
if (code === 'ENOTFOUND') return '域名解析失败'
|
|
379
|
+
}
|
|
380
|
+
const message = error?.message ?? String(error)
|
|
381
|
+
if (/aborted|timeout/i.test(message)) return '请求超时'
|
|
382
|
+
return message
|
|
383
|
+
}
|
|
384
|
+
|
|
329
385
|
/** One JSON GET with a timeout, redirect refusal and a body cap. */
|
|
330
386
|
async function getJson(fetchImpl, url, headers, timeoutMs) {
|
|
331
387
|
const response = await fetchImpl(url, {
|
|
@@ -453,13 +509,17 @@ export function createProviderProbe(options = {}) {
|
|
|
453
509
|
|
|
454
510
|
let body
|
|
455
511
|
let lastError
|
|
512
|
+
let refused = false
|
|
456
513
|
for (const url of listingUrls(profile.baseURL)) {
|
|
457
514
|
try {
|
|
458
515
|
const response = await getJson(fetchImpl, url, headers, timeoutMs)
|
|
459
516
|
if (response.status === 401 || response.status === 403) {
|
|
517
|
+
// Keep going: a relay that rejects the listing still often serves the
|
|
518
|
+
// public pricing endpoint, and its id list is better than nothing.
|
|
460
519
|
diagnostics.status = 'unauthorized'
|
|
461
520
|
diagnostics.message = `HTTP ${response.status}`
|
|
462
|
-
|
|
521
|
+
refused = true
|
|
522
|
+
break
|
|
463
523
|
}
|
|
464
524
|
if (response.status < 200 || response.status >= 300) {
|
|
465
525
|
lastError = new Error(`HTTP ${response.status}`)
|
|
@@ -471,9 +531,34 @@ export function createProviderProbe(options = {}) {
|
|
|
471
531
|
lastError = error
|
|
472
532
|
}
|
|
473
533
|
}
|
|
534
|
+
|
|
535
|
+
// New API exposes model ids and kinds through its public pricing endpoint.
|
|
536
|
+
// When `/models` is unreachable (DNS, timeout, 401, 5xx) that listing is
|
|
537
|
+
// still worth having: it is what lets the page show "served N models"
|
|
538
|
+
// instead of a bare failure. It is also frequently 403/404, which is not an
|
|
539
|
+
// error.
|
|
540
|
+
const pricing = await probePricing(profile, headers)
|
|
474
541
|
if (body === undefined) {
|
|
475
|
-
|
|
476
|
-
|
|
542
|
+
if (pricing.entries.size > 0) {
|
|
543
|
+
// The listing failed but the pricing endpoint enumerated the route.
|
|
544
|
+
diagnostics.status = 'partial'
|
|
545
|
+
diagnostics.models = pricing.entries.size
|
|
546
|
+
diagnostics.capabilities = 0
|
|
547
|
+
diagnostics.pricing = pricing.state
|
|
548
|
+
const status = refused ? String(diagnostics.message ?? '') : undefined
|
|
549
|
+
diagnostics.message = refused
|
|
550
|
+
? `${status === '' ? 'HTTP 401/403' : status}(鉴权被拒),已用价格接口列出模型`
|
|
551
|
+
: `模型列表不可用(${
|
|
552
|
+
lastError === undefined ? '未知原因' : transportMessage(lastError)
|
|
553
|
+
}),已用价格接口列出模型`
|
|
554
|
+
return { entries: withKinds(pricing.entries, pricing.kinds), diagnostics }
|
|
555
|
+
}
|
|
556
|
+
diagnostics.status = refused ? 'unauthorized' : 'failed'
|
|
557
|
+
diagnostics.pricing = pricing.state
|
|
558
|
+
if (!refused) {
|
|
559
|
+
diagnostics.message =
|
|
560
|
+
lastError === undefined ? 'no listing endpoint answered' : transportMessage(lastError)
|
|
561
|
+
}
|
|
477
562
|
return { entries: new Map(), diagnostics }
|
|
478
563
|
}
|
|
479
564
|
|
|
@@ -485,29 +570,60 @@ export function createProviderProbe(options = {}) {
|
|
|
485
570
|
capabilityKeys.some((field) => entry[field] !== undefined),
|
|
486
571
|
).length
|
|
487
572
|
|
|
488
|
-
|
|
489
|
-
|
|
573
|
+
diagnostics.pricing = pricing.state
|
|
574
|
+
return { entries: withKinds(entries, pricing.kinds), diagnostics }
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Fill in the kind of every entry from the pricing endpoint, keeping the
|
|
579
|
+
* richer `/models` facts when both sources describe the same model.
|
|
580
|
+
*/
|
|
581
|
+
/**
|
|
582
|
+
* Return a copy of `entries` with each model's kind filled in from the
|
|
583
|
+
* pricing endpoint, keeping the richer `/models` facts when both sources
|
|
584
|
+
* describe the same model.
|
|
585
|
+
*
|
|
586
|
+
* A copy, not an edit: the partial branch passes the pricing map itself, and
|
|
587
|
+
* mutating it while iterating it would make the result depend on map order.
|
|
588
|
+
*/
|
|
589
|
+
const withKinds = (entries, kinds) => {
|
|
590
|
+
const out = new Map()
|
|
591
|
+
for (const [id, entry] of entries) out.set(id, { ...entry })
|
|
592
|
+
for (const [id, info] of kinds) {
|
|
593
|
+
const entry = out.get(id) ?? { id }
|
|
594
|
+
if (entry.kind === undefined) entry.kind = info.kind
|
|
595
|
+
out.set(id, entry)
|
|
596
|
+
}
|
|
597
|
+
return out
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Read New API's public pricing endpoint for model ids and kinds.
|
|
602
|
+
* @returns `{ entries, kinds, state }`; `state` is ok / disabled / failed.
|
|
603
|
+
*/
|
|
604
|
+
const probePricing = async (profile, headers) => {
|
|
605
|
+
const empty = { entries: new Map(), kinds: new Map(), state: 'failed' }
|
|
606
|
+
let origin
|
|
490
607
|
try {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
608
|
+
origin = new URL(profile.baseURL).origin
|
|
609
|
+
} catch (_error) {
|
|
610
|
+
return { ...empty, state: 'skipped' }
|
|
611
|
+
}
|
|
612
|
+
// The pricing request carries the same Authorization header, so it obeys the
|
|
613
|
+
// same rule as the listing: TLS only, unless the target is loopback.
|
|
614
|
+
if (!isSecureBase(origin) && !isLoopback(origin)) return { ...empty, state: 'skipped' }
|
|
615
|
+
try {
|
|
616
|
+
const response = await getJson(fetchImpl, `${origin}/api/pricing`, headers, timeoutMs)
|
|
617
|
+
if (response.status === 403 || response.status === 404) return { ...empty, state: 'disabled' }
|
|
618
|
+
if (response.status < 200 || response.status >= 300) return empty
|
|
619
|
+
return {
|
|
620
|
+
entries: parsePricingIds(response.body),
|
|
621
|
+
kinds: parsePricing(response.body),
|
|
622
|
+
state: 'ok',
|
|
505
623
|
}
|
|
506
624
|
} catch (_error) {
|
|
507
|
-
|
|
625
|
+
return empty
|
|
508
626
|
}
|
|
509
|
-
|
|
510
|
-
return { entries, diagnostics }
|
|
511
627
|
}
|
|
512
628
|
|
|
513
629
|
const sweep = async () => {
|
package/lib/provider-write.js
CHANGED
|
@@ -23,8 +23,22 @@
|
|
|
23
23
|
* @module lib/provider-write
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Effort keys the provider schema accepts.
|
|
28
|
+
*
|
|
29
|
+
* Must stay in lock-step with `llm-pi-ai`'s `reasoningEfforts` dict key schema.
|
|
30
|
+
* A single unknown key rejects the entire `settings.mutate` op list, which also
|
|
31
|
+
* silently discards the modalities and context window saved alongside it.
|
|
32
|
+
*/
|
|
33
|
+
export const PROVIDER_EFFORT_LEVELS = Object.freeze([
|
|
34
|
+
'off',
|
|
35
|
+
'minimal',
|
|
36
|
+
'low',
|
|
37
|
+
'medium',
|
|
38
|
+
'high',
|
|
39
|
+
'xhigh',
|
|
40
|
+
'max',
|
|
41
|
+
])
|
|
28
42
|
|
|
29
43
|
/** Plain-object guard. */
|
|
30
44
|
function isRecord(value) {
|
|
@@ -43,9 +57,13 @@ function messageOf(error) {
|
|
|
43
57
|
* Convert a manual ladder into the provider `reasoningEfforts` shape.
|
|
44
58
|
*
|
|
45
59
|
* `off` maps to `null` ("supported, send nothing"), every other level maps to
|
|
46
|
-
* its own id as the wire value.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
60
|
+
* its own id as the wire value. Levels the provider schema cannot key are
|
|
61
|
+
* dropped here rather than sent: one unknown key rejects the whole write, and
|
|
62
|
+
* the user would lose the modalities and context window saved with it.
|
|
63
|
+
*
|
|
64
|
+
* Returns undefined when there is nothing worth writing: an unset ladder, a
|
|
65
|
+
* ladder that only offers `off` (the schema rejects that as "offers no level
|
|
66
|
+
* beyond off"), or one whose every level is unusable.
|
|
49
67
|
* @param override - sanitized override entry.
|
|
50
68
|
* @returns a provider ladder, `false` for "no reasoning", or undefined.
|
|
51
69
|
*/
|
|
@@ -55,8 +73,10 @@ export function effortsForProvider(override) {
|
|
|
55
73
|
const ladder = {}
|
|
56
74
|
let beyondOff = false
|
|
57
75
|
for (const effort of override.efforts) {
|
|
58
|
-
const
|
|
59
|
-
if (typeof
|
|
76
|
+
const raw = typeof effort === 'string' ? effort : effort?.id
|
|
77
|
+
if (typeof raw !== 'string' || raw === '') continue
|
|
78
|
+
const id = raw.toLowerCase()
|
|
79
|
+
if (!PROVIDER_EFFORT_LEVELS.includes(id)) continue
|
|
60
80
|
if (id === 'off') ladder.off = null
|
|
61
81
|
else {
|
|
62
82
|
ladder[id] = id
|
|
@@ -68,6 +88,10 @@ export function effortsForProvider(override) {
|
|
|
68
88
|
|
|
69
89
|
/**
|
|
70
90
|
* The provider fields an override implies.
|
|
91
|
+
*
|
|
92
|
+
* These are the only fields this module mirrors; anything else an override
|
|
93
|
+
* carries (`kind`, `defaultEffort`, an off-only ladder) has no provider
|
|
94
|
+
* equivalent and stays in the local override file.
|
|
71
95
|
* @param override - sanitized override entry.
|
|
72
96
|
* @returns `{input?, contextWindow?, reasoningEfforts?}`; empty when nothing is writable.
|
|
73
97
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tzyc/dsh-model-visibility",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"docs"
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
|
-
"test": "node --test test/*.test.mjs",
|
|
27
|
+
"test": "node --import ./test/setup.mjs --test test/*.test.mjs",
|
|
28
28
|
"build": "node scripts/build-client.mjs"
|
|
29
29
|
},
|
|
30
30
|
"dsh": {
|