@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,197 @@
1
+ import { kindOf, sourceOf, sourceLabel, isInferredSource, formatContext, effortNames, defaultEffortName, modalityNames, modalityLabel, resolutionOf, manualFieldsOf } from './format.mjs'
2
+
3
+ function manualTag(model, t) {
4
+ return manualFieldsOf(model).length === 0
5
+ ? null
6
+ : {
7
+ key: 'manual',
8
+ tone: 'accent',
9
+ label: t('tag.manual'),
10
+ title: t('tag.manualTitle'),
11
+ }
12
+ }
13
+
14
+ /** Tags for image / video / embedding / rerank / audio / moderation models. */
15
+ function generationTags(model, t) {
16
+ const kind = kindOf(model)
17
+ const kindSource = sourceOf(model, 'kind')
18
+ const kindLabel = t('tag.kind.' + kind) === 'tag.kind.' + kind ? t('tag.kind.unknown') : t('tag.kind.' + kind)
19
+ const titleKey = kind === 'image' ? 'tag.kindImageTitle' : kind === 'video' ? 'tag.kindVideoTitle' : 'tag.kindTitle'
20
+ const tags = [
21
+ {
22
+ key: 'kind',
23
+ tone: 'kind-' + kind,
24
+ label: kindLabel,
25
+ title: t(titleKey, { kind: kindLabel, source: sourceLabel(kindSource, t) }),
26
+ inferred: isInferredSource(kindSource),
27
+ },
28
+ ]
29
+ const resolution = resolutionOf(model)
30
+ if (resolution !== null && (kind === 'image' || kind === 'video')) {
31
+ tags.push({
32
+ key: 'resolution',
33
+ tone: 'neutral',
34
+ label: resolution,
35
+ title: t('tag.resolutionTitle', { value: resolution }),
36
+ })
37
+ }
38
+ // Image editing capability is the one input fact worth showing on a
39
+ // generation row; it never turns into the chat "multimodal" tag.
40
+ if (kind === 'image' && Array.isArray(model.inputModalities) && model.inputModalities.includes('image')) {
41
+ tags.push({
42
+ key: 'image-input',
43
+ tone: 'neutral',
44
+ label: t('tag.imageInput'),
45
+ title: t('tag.imageInputTitle'),
46
+ })
47
+ }
48
+ const manual = manualTag(model, t)
49
+ if (manual !== null) tags.push(manual)
50
+ return tags
51
+ }
52
+
53
+ /**
54
+ * Per-model capability tags. The same model may know only part of its
55
+ * capabilities: an adapter that cannot answer produces `infoError`, and the
56
+ * unknown facts are shown as such instead of being guessed. Each tag knows
57
+ * whether its value is a declaration (solid border) or a completion from the
58
+ * built-in catalog / name heuristics (dashed border).
59
+ */
60
+ function capabilityTags(model, t) {
61
+ const kind = kindOf(model)
62
+ if (kind !== 'chat') return generationTags(model, t)
63
+
64
+ const tags = []
65
+ const reasoning = model.reasoning
66
+ const levels = effortNames(reasoning)
67
+ const reasoningSource = sourceOf(model, 'reasoning')
68
+ const errorTitle =
69
+ typeof model.infoError === 'string' && model.infoError !== ''
70
+ ? t('tag.infoErrorTitle', { message: model.infoError })
71
+ : t('tag.unknownTitle')
72
+
73
+ if (levels.length > 0) {
74
+ tags.push({
75
+ key: 'reasoning',
76
+ tone: 'accent',
77
+ label: t('tag.reasoning'),
78
+ title: t('tag.reasoningTitle', {
79
+ levels: levels.join(' / '),
80
+ source: sourceLabel(reasoningSource, t),
81
+ }),
82
+ })
83
+ const fallback = defaultEffortName(reasoning)
84
+ tags.push({
85
+ key: 'effort',
86
+ tone: 'neutral',
87
+ label: t('tag.effort') + ' ' + levels.join('/'),
88
+ title:
89
+ fallback === null
90
+ ? t('tag.effortTitleNoFallback', {
91
+ levels: levels.join(' / '),
92
+ source: sourceLabel(reasoningSource, t),
93
+ })
94
+ : t('tag.effortTitle', {
95
+ levels: levels.join(' / '),
96
+ fallback,
97
+ source: sourceLabel(reasoningSource, t),
98
+ }),
99
+ })
100
+ } else if (model.reasoningSupported === true) {
101
+ tags.push({
102
+ key: 'reasoning',
103
+ tone: 'accent',
104
+ label: t('tag.reasoning'),
105
+ title: t('tag.reasoningCatalogTitle', { source: sourceLabel(reasoningSource, t) }),
106
+ inferred: isInferredSource(reasoningSource),
107
+ })
108
+ } else if (model.reasoningSupported === false) {
109
+ tags.push({
110
+ key: 'reasoning',
111
+ tone: 'muted',
112
+ label: t('tag.noReasoning'),
113
+ title: sourceLabel(reasoningSource, t),
114
+ inferred: isInferredSource(reasoningSource),
115
+ })
116
+ } else {
117
+ tags.push({ key: 'reasoning', tone: 'muted', label: t('tag.reasoningUnknown'), title: errorTitle })
118
+ }
119
+
120
+ const contextSource = sourceOf(model, 'context')
121
+ const context = formatContext(model.contextWindow)
122
+ tags.push({
123
+ key: 'context',
124
+ tone: context === null ? 'muted' : 'neutral',
125
+ label: t('tag.context') + ' ' + (context === null ? t('tag.unknown') : context),
126
+ title:
127
+ context === null
128
+ ? errorTitle
129
+ : t('tag.contextTitle', {
130
+ tokens: Number(model.contextWindow).toLocaleString(),
131
+ source: sourceLabel(contextSource, t),
132
+ }),
133
+ inferred: context !== null && isInferredSource(contextSource),
134
+ })
135
+
136
+ const modalities = Array.isArray(model.inputModalities) ? model.inputModalities : null
137
+ const modalitySource = sourceOf(model, 'modalities')
138
+ if (modalities === null || modalities.length === 0) {
139
+ tags.push({ key: 'modality', tone: 'muted', label: t('tag.modalityUnknown'), title: errorTitle })
140
+ } else {
141
+ const vision = modalities.includes('image')
142
+ tags.push({
143
+ key: 'modality',
144
+ tone: vision ? 'vision' : 'neutral',
145
+ label: vision ? t('tag.multimodal') : t('tag.textOnly'),
146
+ title: t('tag.modalityTitle', {
147
+ modalities: modalityNames(modalities, t),
148
+ source: sourceLabel(modalitySource, t),
149
+ }),
150
+ inferred: isInferredSource(modalitySource),
151
+ })
152
+ }
153
+
154
+ // A vendor-documented correction is the one non-dashed value that is not
155
+ // an adapter declaration; keep it visible so "why is this text-only?"
156
+ // has an answer next to the tag.
157
+ const correction = model.correction
158
+ if (correction !== null && typeof correction === 'object' && typeof correction.code === 'string') {
159
+ const noteKey = 'correction.' + correction.code
160
+ const localized = t(noteKey)
161
+ const note = localized === noteKey
162
+ ? typeof correction.note === 'string' && correction.note !== ''
163
+ ? correction.note
164
+ : correction.code
165
+ : localized
166
+ tags.push({
167
+ key: 'correction',
168
+ tone: 'muted',
169
+ label: t('tag.correction'),
170
+ title: t('tag.correctionTitle', {
171
+ note,
172
+ source: typeof correction.source === 'string' && correction.source !== '' ? correction.source : '—',
173
+ }),
174
+ })
175
+ }
176
+ const manual = manualTag(model, t)
177
+ if (manual !== null) tags.push(manual)
178
+ return tags
179
+ }
180
+
181
+ function tagClassOf(tag) {
182
+ const tone =
183
+ tag.tone === 'accent'
184
+ ? ' mvp_tag--accent'
185
+ : tag.tone === 'vision'
186
+ ? ' mvp_tag--vision'
187
+ : tag.tone === 'kind'
188
+ ? ' mvp_tag--kind'
189
+ : typeof tag.tone === 'string' && tag.tone.startsWith('kind-')
190
+ ? ' mvp_tag--kind mvp_tag--' + tag.tone
191
+ : tag.tone === 'muted'
192
+ ? ' mvp_tag--muted'
193
+ : ''
194
+ return 'mvp_tag' + tone + (tag.inferred === true ? ' mvp_tag--inferred' : '')
195
+ }
196
+
197
+ export { capabilityTags, tagClassOf }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Token-usage strip with the one-click exports.
3
+ *
4
+ * The strip renders the ledger's today/week/month/total buckets and links the
5
+ * two CSV endpoints. It is an inner piece of the settings section: the section
6
+ * supplies the translator and the usage store instance.
7
+ *
8
+ * @module client-src/usage-view
9
+ */
10
+
11
+ const React = require('react')
12
+ const h = React.createElement
13
+
14
+ import { API } from './constants.mjs'
15
+ import { formatTokens } from './pure.mjs'
16
+ import { useSource } from './hooks.mjs'
17
+
18
+ /** Render one [in, out] window as "输入 x · 输出 y". */
19
+ function rangeText(range, t) {
20
+ if (!Array.isArray(range) || range.every((value) => value === 0)) return t('usage.none')
21
+ const input = (range[0] ?? 0) + (range[2] ?? 0) + (range[3] ?? 0)
22
+ const output = range[1] ?? 0
23
+ return t('usage.range', {
24
+ input: formatTokens(input),
25
+ output: formatTokens(output),
26
+ total: formatTokens(input + output),
27
+ })
28
+ }
29
+
30
+ /** Trigger a same-origin download without leaving the page. */
31
+ function download(href) {
32
+ const anchor = document.createElement('a')
33
+ anchor.href = href
34
+ anchor.rel = 'noopener'
35
+ document.body.appendChild(anchor)
36
+ anchor.click()
37
+ anchor.remove()
38
+ }
39
+
40
+ export function UsageToolbar({ usage, t, features }) {
41
+ const state = useSource(usage)
42
+ const supported = features.length === 0 || features.includes('usage')
43
+ const ranges = state.snapshot?.ranges
44
+ const computing = state.snapshot?.computing === true
45
+ return h(
46
+ 'div',
47
+ { className: 'mvp_usage' + (supported ? '' : ' mvp_usage--off') },
48
+ h(
49
+ 'div',
50
+ { className: 'mvp_usageHead' },
51
+ h('span', { className: 'mvp_usageTitle' }, t('usage.title')),
52
+ computing ? h('span', { className: 'mvp_usageComputing' }, t('usage.computing')) : null,
53
+ h(
54
+ 'span',
55
+ { className: 'mvp_usageActions' },
56
+ h(
57
+ 'button',
58
+ {
59
+ type: 'button',
60
+ className: 'mvp_button',
61
+ title: t('usage.exportUsageTitle'),
62
+ onClick: () => download(`${API}/export?type=usage`),
63
+ },
64
+ t('usage.exportUsage'),
65
+ ),
66
+ h(
67
+ 'button',
68
+ {
69
+ type: 'button',
70
+ className: 'mvp_button',
71
+ title: t('usage.exportModelsTitle'),
72
+ onClick: () => download(`${API}/export?type=models`),
73
+ },
74
+ t('usage.exportModels'),
75
+ ),
76
+ ),
77
+ ),
78
+ supported
79
+ ? h(
80
+ 'div',
81
+ { className: 'mvp_usageRanges' },
82
+ h(
83
+ 'span',
84
+ { className: 'mvp_stat', title: t('usage.todayTitle') },
85
+ h('span', { className: 'mvp_statLabel' }, t('usage.today')),
86
+ h('span', { className: 'mvp_statValue' }, rangeText(ranges?.today, t)),
87
+ ),
88
+ h(
89
+ 'span',
90
+ { className: 'mvp_stat', title: t('usage.weekTitle') },
91
+ h('span', { className: 'mvp_statLabel' }, t('usage.week')),
92
+ h('span', { className: 'mvp_statValue' }, rangeText(ranges?.week, t)),
93
+ ),
94
+ h(
95
+ 'span',
96
+ { className: 'mvp_stat', title: t('usage.monthTitle') },
97
+ h('span', { className: 'mvp_statLabel' }, t('usage.month')),
98
+ h('span', { className: 'mvp_statValue' }, rangeText(ranges?.month, t)),
99
+ ),
100
+ h(
101
+ 'span',
102
+ { className: 'mvp_stat mvp_stat--total', title: t('usage.totalTitle') },
103
+ h('span', { className: 'mvp_statLabel' }, t('usage.total')),
104
+ h('span', { className: 'mvp_statValue' }, rangeText(ranges?.total, t)),
105
+ ),
106
+ )
107
+ : h('div', { className: 'mvp_usageHint' }, t('usage.hostStale')),
108
+ state.error !== null
109
+ ? h('div', { className: 'mvp_usageHint mvp_usageHint--warn' }, state.error)
110
+ : null,
111
+ )
112
+ }