@tzyc/dsh-model-visibility 1.9.1 → 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.
- 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 +581 -0
- package/client-src/tags.mjs +197 -0
- package/client-src/usage-view.mjs +112 -0
- package/client.js +3228 -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
|
@@ -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
|
+
}
|