@tzyc/dsh-model-visibility 1.9.0 → 1.10.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.
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Plugin entry: store wiring, facts inputs and effect registration.
3
+ *
4
+ * Assembled with the other modules by `scripts/build-client.mjs`; the bundle
5
+ * factory returns `{ inject, apply }` exactly like the original single-file
6
+ * bundle, but every subsystem now lives in its own module.
7
+ */
8
+
9
+ const React = require('react')
10
+ const h = React.createElement
11
+
12
+ import { NS, ENTRY, SECTION_ID } from './constants.mjs'
13
+ import { DICT } from './dict.mjs'
14
+ import { CSS } from './css.mjs'
15
+ import { createCatalog, createOverrides, createUsageApi, createVerifyApi } from './catalog-store.mjs'
16
+ import { makeSection } from './settings-section.mjs'
17
+ import { installComposerFilter } from './composer-filter.mjs'
18
+
19
+ const inject = ['slots', 'locale', 'configForms', 'remote', 'remote.session']
20
+
21
+ function apply(ctx) {
22
+ ctx.effect(() => ctx.locale.register(NS, DICT), 'model-visibility: dictionaries')
23
+
24
+ ctx.effect(() => {
25
+ const tag = document.createElement('style')
26
+ tag.dataset.plugin = 'dsh-model-visibility'
27
+ tag.textContent = CSS
28
+ document.head.appendChild(tag)
29
+ return () => {
30
+ tag.remove()
31
+ }
32
+ }, 'model-visibility: styles')
33
+
34
+ const form = ctx.configForms.get(ENTRY)
35
+ const catalog = createCatalog(ctx)
36
+ const overrides = createOverrides()
37
+ const usage = createUsageApi()
38
+ const verify = createVerifyApi()
39
+
40
+ ctx.effect(() => {
41
+ const disposers = []
42
+ disposers.push(
43
+ ctx.on('connection/reset', () => {
44
+ catalog.refresh()
45
+ overrides.load()
46
+ usage.refresh()
47
+ }),
48
+ )
49
+ if (ctx.remote !== undefined && typeof ctx.remote.$on === 'function') {
50
+ disposers.push(
51
+ ctx.remote.$on('llm/adapters-updated', () => {
52
+ catalog.refresh()
53
+ }),
54
+ )
55
+ for (const event of [
56
+ 'settings/document-updated',
57
+ 'credentials/record-updated',
58
+ 'credentials/reference-updated',
59
+ ]) {
60
+ disposers.push(
61
+ ctx.remote.$on(event, () => {
62
+ catalog.load()
63
+ }),
64
+ )
65
+ }
66
+ }
67
+ return () => {
68
+ for (const dispose of disposers) {
69
+ try {
70
+ dispose()
71
+ } catch (_error) {
72
+ // A source may already be gone during teardown.
73
+ }
74
+ }
75
+ }
76
+ }, 'model-visibility: catalog refresh')
77
+
78
+ const Section = makeSection(form, catalog, overrides, usage, verify)
79
+ ctx.effect(
80
+ () =>
81
+ ctx.slots.inject('settings.section', () =>
82
+ ctx.slots.register(
83
+ {
84
+ name: 'settings.section',
85
+ id: SECTION_ID,
86
+ order: 12,
87
+ locale: NS,
88
+ label: () => ctx.locale.bind(NS)('nav'),
89
+ },
90
+ Section,
91
+ ),
92
+ ),
93
+ 'model-visibility: settings section',
94
+ )
95
+
96
+ ctx.inject(['modelDirectories'], (scope) => {
97
+ installComposerFilter(scope, form)
98
+ })
99
+ }
100
+
101
+ export { inject, apply }
@@ -0,0 +1,286 @@
1
+ const React = require('react')
2
+ const h = React.createElement
3
+
4
+ import { modalityLabel, effortLabel, OVERRIDE_MODALITIES, OVERRIDE_EFFORTS } from './format.mjs'
5
+ function providerWriteNote(result, t) {
6
+ if (result === null || typeof result !== 'object') return null
7
+ if (result.written === true) {
8
+ const fields = (Array.isArray(result.fields) ? result.fields : [])
9
+ .map((field) => t('override.field.' + field))
10
+ .join('、')
11
+ return t('override.providerWritten', { fields })
12
+ }
13
+ if (result.reason === 'nothing' || result.reason === 'skipped') return null
14
+ const label = t('override.reason.' + String(result.reason))
15
+ return t('override.providerSkipped', {
16
+ reason: label === 'override.reason.' + String(result.reason) ? String(result.reason) : label,
17
+ })
18
+ }
19
+
20
+ function ModelOverrideEditor({ entry, name, t, overrides, onClose, onSaved }) {
21
+ const [kind, setKind] = React.useState(entry.kind || '')
22
+ const [context, setContext] = React.useState(
23
+ entry.contextWindow === undefined ? '' : String(entry.contextWindow),
24
+ )
25
+ const [input, setInput] = React.useState(
26
+ () => new Set(Array.isArray(entry.input) ? entry.input : []),
27
+ )
28
+ const [efforts, setEfforts] = React.useState(
29
+ () => new Set((Array.isArray(entry.efforts) ? entry.efforts : []).map((item) => item.id)),
30
+ )
31
+ const [noReasoning, setNoReasoning] = React.useState(entry.reasoningSupported === false)
32
+ const [defaultEffort, setDefaultEffort] = React.useState(
33
+ typeof entry.defaultEffort === 'string' ? entry.defaultEffort : '',
34
+ )
35
+ const [busy, setBusy] = React.useState(false)
36
+ const [error, setError] = React.useState(null)
37
+ const [applyToProvider, setApplyToProvider] = React.useState(true)
38
+
39
+ const toggleIn = (setter, id) => {
40
+ setter((previous) => {
41
+ const next = new Set(previous)
42
+ if (next.has(id)) next.delete(id)
43
+ else next.add(id)
44
+ return next
45
+ })
46
+ }
47
+
48
+ const fail = (failure) => {
49
+ setBusy(false)
50
+ setError(
51
+ t('override.failed', {
52
+ message: String(failure && failure.message ? failure.message : failure),
53
+ }),
54
+ )
55
+ }
56
+
57
+ const save = () => {
58
+ const payload = { provider: entry.provider, model: entry.model }
59
+ if (kind !== '') payload.kind = kind
60
+ if (context.trim() !== '') {
61
+ const value = Number(context)
62
+ if (!Number.isFinite(value) || value <= 0) {
63
+ setError(t('override.contextInvalid'))
64
+ return
65
+ }
66
+ payload.contextWindow = Math.round(value)
67
+ }
68
+ if (input.size > 0) {
69
+ payload.input = OVERRIDE_MODALITIES.filter((id) => input.has(id))
70
+ }
71
+ if (noReasoning) {
72
+ payload.reasoningSupported = false
73
+ } else if (efforts.size > 0) {
74
+ payload.efforts = OVERRIDE_EFFORTS.filter((id) => efforts.has(id)).map((id) => ({ id }))
75
+ if (defaultEffort !== '' && efforts.has(defaultEffort)) {
76
+ payload.defaultEffort = defaultEffort
77
+ }
78
+ }
79
+ payload.applyToProvider = applyToProvider
80
+ setBusy(true)
81
+ setError(null)
82
+ Promise.resolve(overrides.save(payload)).then(
83
+ (result) => {
84
+ setBusy(false)
85
+ if (typeof onSaved === 'function') onSaved(result)
86
+ },
87
+ fail,
88
+ )
89
+ }
90
+
91
+ const resetToAuto = () => {
92
+ setBusy(true)
93
+ setError(null)
94
+ Promise.resolve(overrides.remove(entry.provider, entry.model)).then(
95
+ () => {
96
+ setBusy(false)
97
+ if (typeof onSaved === 'function') onSaved()
98
+ },
99
+ fail,
100
+ )
101
+ }
102
+
103
+ const kindOptions = [['', t('override.kindAuto')]].concat(
104
+ ['chat', 'image', 'video', 'embedding', 'rerank', 'audio', 'classifier', 'translation'].map(
105
+ (id) => [id, id === 'chat' ? t('page.filter.chat') : t('tag.kind.' + id)],
106
+ ),
107
+ )
108
+ const defaultOptions = [['', t('override.defaultEffortAuto')]].concat(
109
+ OVERRIDE_EFFORTS.filter((id) => efforts.has(id)).map((id) => [id, effortLabel(id, t)]),
110
+ )
111
+ const selectedDefault = efforts.has(defaultEffort) ? defaultEffort : ''
112
+
113
+ return h(
114
+ 'div',
115
+ { className: 'mvp_editor' },
116
+ h(
117
+ 'div',
118
+ { className: 'mvp_editorHead' },
119
+ h('div', { className: 'mvp_editorTitle' }, t('override.title') + ' · ' + name),
120
+ h('div', { className: 'mvp_editorSub' }, t('override.hint')),
121
+ ),
122
+ h(
123
+ 'div',
124
+ { className: 'mvp_editorGrid' },
125
+ h(
126
+ 'label',
127
+ { className: 'mvp_field' },
128
+ h('span', { className: 'mvp_fieldLabel' }, t('override.kind')),
129
+ h(
130
+ 'select',
131
+ {
132
+ className: 'mvp_select',
133
+ value: kind,
134
+ 'aria-label': t('override.kind'),
135
+ onChange: (event) => setKind(event.target.value),
136
+ },
137
+ kindOptions.map(([value, label]) => h('option', { key: value, value }, label)),
138
+ ),
139
+ ),
140
+ h(
141
+ 'label',
142
+ { className: 'mvp_field' },
143
+ h('span', { className: 'mvp_fieldLabel' }, t('override.context')),
144
+ h('input', {
145
+ className: 'mvp_number',
146
+ type: 'number',
147
+ min: '1',
148
+ step: '1',
149
+ value: context,
150
+ placeholder: t('override.contextPlaceholder'),
151
+ 'aria-label': t('override.context'),
152
+ onInput: (event) => setContext(event.target.value),
153
+ }),
154
+ ),
155
+ h(
156
+ 'div',
157
+ { className: 'mvp_field mvp_field--wide' },
158
+ h('span', { className: 'mvp_fieldLabel' }, t('override.input')),
159
+ h(
160
+ 'div',
161
+ { className: 'mvp_chips' },
162
+ OVERRIDE_MODALITIES.map((id) =>
163
+ h(
164
+ 'button',
165
+ {
166
+ key: id,
167
+ type: 'button',
168
+ className: 'mvp_chip',
169
+ 'aria-pressed': input.has(id),
170
+ disabled: busy,
171
+ onClick: () => toggleIn(setInput, id),
172
+ },
173
+ modalityLabel(id, t),
174
+ ),
175
+ ),
176
+ ),
177
+ ),
178
+ h(
179
+ 'div',
180
+ { className: 'mvp_field mvp_field--wide' },
181
+ h('span', { className: 'mvp_fieldLabel' }, t('override.efforts')),
182
+ h(
183
+ 'div',
184
+ { className: 'mvp_chips' },
185
+ h(
186
+ 'button',
187
+ {
188
+ type: 'button',
189
+ className: 'mvp_chip',
190
+ 'aria-pressed': noReasoning,
191
+ disabled: busy,
192
+ onClick: () => setNoReasoning((value) => !value),
193
+ },
194
+ t('tag.noReasoning'),
195
+ ),
196
+ OVERRIDE_EFFORTS.map((id) =>
197
+ h(
198
+ 'button',
199
+ {
200
+ key: id,
201
+ type: 'button',
202
+ className: 'mvp_chip',
203
+ 'aria-pressed': !noReasoning && efforts.has(id),
204
+ disabled: busy || noReasoning,
205
+ onClick: () => toggleIn(setEfforts, id),
206
+ },
207
+ effortLabel(id, t),
208
+ ),
209
+ ),
210
+ ),
211
+ ),
212
+ !noReasoning && efforts.size > 0
213
+ ? h(
214
+ 'label',
215
+ { className: 'mvp_field' },
216
+ h('span', { className: 'mvp_fieldLabel' }, t('override.defaultEffort')),
217
+ h(
218
+ 'select',
219
+ {
220
+ className: 'mvp_select',
221
+ value: selectedDefault,
222
+ 'aria-label': t('override.defaultEffort'),
223
+ onChange: (event) => setDefaultEffort(event.target.value),
224
+ },
225
+ defaultOptions.map(([value, label]) => h('option', { key: value, value }, label)),
226
+ ),
227
+ )
228
+ : null,
229
+ h(
230
+ 'div',
231
+ { className: 'mvp_field mvp_field--wide' },
232
+ h(
233
+ 'label',
234
+ { className: 'mvp_check' },
235
+ h('input', {
236
+ type: 'checkbox',
237
+ checked: applyToProvider,
238
+ disabled: busy,
239
+ onChange: (event) => setApplyToProvider(event.target.checked),
240
+ }),
241
+ h('span', null, t('override.provider')),
242
+ ),
243
+ h('div', { className: 'mvp_editorNote' }, t('override.providerHint')),
244
+ ),
245
+ ),
246
+ error !== null ? h('div', { className: 'mvp_editorError', role: 'alert' }, error) : null,
247
+ h(
248
+ 'div',
249
+ { className: 'mvp_editorFooter' },
250
+ h('div', { className: 'mvp_editorNote' }, t('override.effortHint')),
251
+ h(
252
+ 'div',
253
+ { className: 'mvp_editorActions' },
254
+ h(
255
+ 'button',
256
+ {
257
+ type: 'button',
258
+ className: 'mvp_button',
259
+ disabled: busy,
260
+ title: t('override.resetTitle'),
261
+ onClick: resetToAuto,
262
+ },
263
+ t('override.reset'),
264
+ ),
265
+ h(
266
+ 'button',
267
+ { type: 'button', className: 'mvp_button', disabled: busy, onClick: onClose },
268
+ t('override.cancel'),
269
+ ),
270
+ h(
271
+ 'button',
272
+ {
273
+ type: 'button',
274
+ className: 'mvp_button mvp_button--accent',
275
+ disabled: busy,
276
+ onClick: save,
277
+ },
278
+ t(busy ? 'override.saving' : 'override.save'),
279
+ ),
280
+ ),
281
+ ),
282
+ )
283
+ }
284
+
285
+ export { ModelOverrideEditor, providerWriteNote }
286
+
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Pure helpers that never touch React or the DOM, so `node --test` can call
3
+ * them directly.
4
+ *
5
+ * @module client-src/pure
6
+ */
7
+
8
+ /** Number of one bucket's token sum, guarding partial buckets. */
9
+ export function bucketTotalOf(buckets) {
10
+ if (!Array.isArray(buckets)) return 0
11
+ return buckets.reduce((sum, value) => sum + (Number.isFinite(value) ? value : 0), 0)
12
+ }
13
+
14
+ /** 1234567 → "1.2M"; 4096 → "4.1K"; 0/undefined → "0". */
15
+ export function formatTokens(value) {
16
+ const tokens = Number.isFinite(value) ? value : 0
17
+ if (tokens >= 1_000_000) {
18
+ const millions = Math.round((tokens / 100_000)) / 10
19
+ return Number.isInteger(millions) ? `${millions}M` : millions.toFixed(1) + 'M'
20
+ }
21
+ if (tokens >= 1_000) {
22
+ const thousands = Math.round(tokens / 100) / 10
23
+ return Number.isInteger(thousands) ? `${thousands}K` : thousands.toFixed(1) + 'K'
24
+ }
25
+ return String(tokens)
26
+ }
27
+
28
+ /** Local-calendar `YYYY-MM-DD` stamp used by the ledger's day buckets. */
29
+ export function dayKeyOf(time) {
30
+ const date = new Date(Number.isFinite(time) ? time : Date.now())
31
+ if (Number.isNaN(date.getTime())) return ''
32
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
33
+ }
34
+
35
+ /** First day (Monday) of the local ISO week of `time`. */
36
+ export function weekKeyOf(time) {
37
+ const date = new Date(Number.isFinite(time) ? time : Date.now())
38
+ const weekday = (date.getDay() + 6) % 7
39
+ date.setDate(date.getDate() - weekday)
40
+ return dayKeyOf(date.getTime())
41
+ }
42
+
43
+ /** Local calendar month prefix of `time`. */
44
+ export function monthPrefixOf(time) {
45
+ return dayKeyOf(time).slice(0, 7)
46
+ }
47
+
48
+ /**
49
+ * Apply the section's filters to one catalog.
50
+ *
51
+ * Mirrors the settings page exactly; the composer filter is a separate,
52
+ * simpler pass (visibility only). Inputs are the loaded groups plus the five
53
+ * filter axes; output keeps the group objects in order with only kept models.
54
+ */
55
+ export function computeFilteredGroups(groups, { filter = 'all', query = '', multimodalOnly = false, reasoningOnly = false, minContext = 0 } = {}) {
56
+ const normalized = String(query ?? '').trim().toLowerCase()
57
+ const filtered = []
58
+ for (const group of groups ?? []) {
59
+ const models = []
60
+ for (const model of group.models ?? []) {
61
+ if (filter !== 'all' && bucket(model) !== filter) continue
62
+ if (normalized !== '') {
63
+ const name = String(model.name ?? '').toLowerCase()
64
+ const id = String(model.id ?? '').toLowerCase()
65
+ if (!name.includes(normalized) && !id.includes(normalized)) continue
66
+ }
67
+ if (multimodalOnly && !(Array.isArray(model.inputModalities) && model.inputModalities.includes('image'))) {
68
+ continue
69
+ }
70
+ if (reasoningOnly && model.reasoningSupported !== true) continue
71
+ if (minContext > 0 && !(Number.isFinite(model.contextWindow) && model.contextWindow >= minContext)) {
72
+ continue
73
+ }
74
+ models.push(model)
75
+ }
76
+ if (models.length > 0) filtered.push({ ...group, models })
77
+ }
78
+ return filtered
79
+ }
80
+
81
+ function bucket(model) {
82
+ const kind = model && typeof model.kind === 'string' && model.kind !== '' ? model.kind : 'chat'
83
+ if (kind === 'image') return 'image'
84
+ if (kind === 'video') return 'video'
85
+ if (kind === 'chat') return 'chat'
86
+ return 'other'
87
+ }
88
+
89
+ /** Counts per kind bucket for the whole list. */
90
+ export function countKinds(models) {
91
+ const counts = { chat: 0, image: 0, video: 0, other: 0 }
92
+ for (const model of models ?? []) counts[bucket(model)] += 1
93
+ return counts
94
+ }
95
+
96
+ /** Checker-model candidates from a loaded catalog state. */
97
+ export function checkerRoutesOf(groups) {
98
+ const routes = []
99
+ for (const group of groups ?? []) {
100
+ if (group === null || typeof group !== 'object') continue
101
+ const models = Array.isArray(group.models) ? group.models : []
102
+ if (models.length > 0) routes.push({ id: group.id, name: group.name ?? group.id })
103
+ }
104
+ return routes
105
+ }
106
+
107
+ /** Compact label + tooltip for one provider probe diagnostic. */
108
+ export function probeOf(state, group, t) {
109
+ const list = Array.isArray(state.providers) ? state.providers : []
110
+ const diagnostic = list.find((item) => item && item.id === group.id)
111
+ if (diagnostic === undefined) return null
112
+ const details = [
113
+ diagnostic.host ? 'host: ' + diagnostic.host : null,
114
+ diagnostic.status ? 'status: ' + diagnostic.status : null,
115
+ diagnostic.message ? 'message: ' + diagnostic.message : null,
116
+ diagnostic.pricing && diagnostic.pricing !== 'skipped' ? 'pricing: ' + diagnostic.pricing : null,
117
+ ]
118
+ .filter(Boolean)
119
+ .join('\n')
120
+ if (diagnostic.status === 'ok') {
121
+ return {
122
+ label:
123
+ Number(diagnostic.capabilities) > 0
124
+ ? t('group.probeOk', {
125
+ models: diagnostic.models ?? 0,
126
+ caps: diagnostic.capabilities,
127
+ })
128
+ : t('group.probeNoCaps', { models: diagnostic.models ?? 0 }),
129
+ title: details,
130
+ }
131
+ }
132
+ if (diagnostic.status === 'failed' || diagnostic.status === 'unauthorized') {
133
+ return { label: t('group.probeFailed'), title: details }
134
+ }
135
+ return { label: t('group.probeSkipped'), title: details }
136
+ }