@tzyc/dsh-model-visibility 0.0.0-stage → 1.9.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/LICENSE +21 -0
- package/README.md +367 -2
- package/client.js +2258 -0
- package/cordis.patch.yml +7 -0
- package/docs/capability-tags.png +0 -0
- package/docs/icon-180.png +0 -0
- package/docs/settings-page.png +0 -0
- package/icon.svg +9 -0
- package/index.js +476 -0
- package/lib/capability-catalog.js +137 -0
- package/lib/capability-corrections.js +65 -0
- package/lib/image-input-gate.js +113 -0
- package/lib/model-catalog.js +670 -0
- package/lib/model-info.js +786 -0
- package/lib/model-overrides.js +262 -0
- package/lib/provider-probe.js +596 -0
- package/lib/provider-write.js +175 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +64 -6
- package/screenshots.json +4 -0
package/client.js
ADDED
|
@@ -0,0 +1,2258 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-model-visibility',
|
|
3
|
+
factory(require) {
|
|
4
|
+
const React = require('react')
|
|
5
|
+
const h = React.createElement
|
|
6
|
+
|
|
7
|
+
/** Dictionary namespace owned by this plugin. */
|
|
8
|
+
const NS = 'model-visibility'
|
|
9
|
+
/** Host entry id from cordis.patch.yml; also the settings namespace. */
|
|
10
|
+
const ENTRY = 'model-visibility'
|
|
11
|
+
/** Config field persisted by the Host settings document. */
|
|
12
|
+
const FIELD = 'hidden'
|
|
13
|
+
/** Settings section id. */
|
|
14
|
+
const SECTION_ID = 'model-visibility'
|
|
15
|
+
/**
|
|
16
|
+
* Same-origin route the Host half registers through `connection.fetch`.
|
|
17
|
+
* Resolved against the page base so a reverse-proxy prefix is preserved.
|
|
18
|
+
*/
|
|
19
|
+
const API = (() => {
|
|
20
|
+
try {
|
|
21
|
+
return new URL('api/dsh-model-visibility', document.baseURI).pathname
|
|
22
|
+
} catch (_error) {
|
|
23
|
+
return '/api/dsh-model-visibility'
|
|
24
|
+
}
|
|
25
|
+
})()
|
|
26
|
+
|
|
27
|
+
const DICT = {
|
|
28
|
+
zh: {
|
|
29
|
+
'nav': '模型显示',
|
|
30
|
+
'page.title': '模型显示',
|
|
31
|
+
'page.description':
|
|
32
|
+
'关闭的模型不会出现在聊天输入框的模型下拉列表中;这里的改动立即生效,之后仍可随时重新打开。每个模型标注真实能力:适配器声明优先,其次读中转站的模型列表,再用内置目录补全;生图、生视频等生成类模型单独标记。',
|
|
33
|
+
'page.summary': '已显示 {visible} / {total} 个模型',
|
|
34
|
+
'page.hiddenSummary': '已关闭 {count} 个',
|
|
35
|
+
'page.kindSummary': '{count} 个生图/生视频模型',
|
|
36
|
+
'page.loading': '正在加载模型列表…',
|
|
37
|
+
'page.empty': '暂无可用模型。',
|
|
38
|
+
'page.filterEmpty': '当前筛选下没有模型。',
|
|
39
|
+
'page.error': '模型列表加载失败:{message}',
|
|
40
|
+
'page.saveFailed': '保存失败,请重试。',
|
|
41
|
+
'page.refresh': '刷新',
|
|
42
|
+
'page.refreshing': '刷新中…',
|
|
43
|
+
'page.reload': '重新获取能力',
|
|
44
|
+
'page.reloading': '正在获取…',
|
|
45
|
+
'page.reloadTitle':
|
|
46
|
+
'清空本插件的能力缓存和正在进行的读取,重新询问适配器与所有中转站;不会改动任何模型开关。适配器没有公布的思考强度仍不会凭空出现。',
|
|
47
|
+
'page.reloadUnavailable': 'Host 半不支持强制重新获取;重启 DSH 后可用。',
|
|
48
|
+
'page.collapseGroups': '折叠全部分组',
|
|
49
|
+
'page.expandGroups': '展开全部分组',
|
|
50
|
+
'page.collapseGroupsTitle': '只保留分组标题,方便跳到其他分组',
|
|
51
|
+
'page.expandGroupsTitle': '展开所有被折叠的分组',
|
|
52
|
+
'page.showAll': '全部显示',
|
|
53
|
+
'page.legend':
|
|
54
|
+
'标签说明:推理 = 可调推理等级;上下文 = 输入窗口;多模态 = 可输入图像。实线标签来自适配器、中转站或厂商文档,虚线标签来自内置目录或模型名推断。生图 / 生视频 / 嵌入等模型只标类型,不显示思考强度与文本标签。',
|
|
55
|
+
'page.metaFallback':
|
|
56
|
+
'模型能力信息暂不可用:上下文与模态显示为未知,推理档位仍来自官方目录。',
|
|
57
|
+
'page.hostStale':
|
|
58
|
+
'Host 半仍是旧版本:颜色与开关已更新,但「放行图像」和官方校正要等 DSH 重启(或 profile 重组)后才会生效。',
|
|
59
|
+
'page.providerFailures': '部分提供方暂时读不到模型:{names}',
|
|
60
|
+
'page.probeSummary': '中转站探测:{ok} 个已读取模型列表,{caps} 个公布了能力字段',
|
|
61
|
+
'page.probeSummaryNone': '中转站探测:暂无可读取的路由配置',
|
|
62
|
+
'page.catalogNote': '内置目录:pi-ai {version} 快照({date}),共 {models} 个模型',
|
|
63
|
+
'page.applyInput': '让输入框按能力标注放行图像',
|
|
64
|
+
'page.applyInputHint':
|
|
65
|
+
'开启后,被标注为「多模态」的中转站对话模型会在发送请求时自动补上图像输入能力;关闭则完全按 Provider 配置里的 input 声明。只影响中转站(pi-ai)路由,不改任何配置文件;若某个上游实际拒绝图像,关闭此开关即可。',
|
|
66
|
+
'page.kindLegend':
|
|
67
|
+
'颜色区分模型类型:生图 蓝 · 生视频 绿 · 嵌入 橙 · 音频 紫 · 重排 红 · 审核 玫红 · 翻译 灰。「官方校正」表示内置目录与厂商文档不一致时以文档为准。',
|
|
68
|
+
'page.filterLabel': '筛选',
|
|
69
|
+
'page.searchPlaceholder': '搜索模型名称或 ID',
|
|
70
|
+
'page.capabilityLabel': '能力',
|
|
71
|
+
'page.capability.multimodal': '多模态',
|
|
72
|
+
'page.capability.reasoning': '支持推理',
|
|
73
|
+
'page.contextLabel': '上下文至少',
|
|
74
|
+
'page.context.any': '不限',
|
|
75
|
+
'page.context.128k': '128K+',
|
|
76
|
+
'page.context.256k': '256K+',
|
|
77
|
+
'page.context.512k': '512K+',
|
|
78
|
+
'page.context.1m': '1M+',
|
|
79
|
+
'page.filter.all': '全部',
|
|
80
|
+
'page.filter.chat': '对话',
|
|
81
|
+
'page.filter.image': '生图',
|
|
82
|
+
'page.filter.video': '生视频',
|
|
83
|
+
'page.filter.other': '其它',
|
|
84
|
+
'group.showAll': '全组显示',
|
|
85
|
+
'group.hideAll': '全组隐藏',
|
|
86
|
+
'group.showFiltered': '显示筛选结果',
|
|
87
|
+
'group.hideFiltered': '隐藏筛选结果',
|
|
88
|
+
'group.collapseNamed': '折叠 {name} 分组',
|
|
89
|
+
'group.expandNamed': '展开 {name} 分组',
|
|
90
|
+
'group.capabilities': '{chat} 对话 · {image} 生图 · {video} 生视频 · {other} 其它',
|
|
91
|
+
'group.probeOk': '中转站 {models} 个模型 · {caps} 个带能力字段',
|
|
92
|
+
'group.probeNoCaps': '中转站 {models} 个模型 · 未公布能力字段',
|
|
93
|
+
'group.probeFailed': '中转站读取失败',
|
|
94
|
+
'group.probeSkipped': '未探测中转站',
|
|
95
|
+
'tag.reasoning': '支持推理',
|
|
96
|
+
'tag.noReasoning': '不支持推理',
|
|
97
|
+
'tag.reasoningUnknown': '推理未知',
|
|
98
|
+
'tag.effort': '思考强度',
|
|
99
|
+
'tag.context': '上下文',
|
|
100
|
+
'tag.unknown': '未知',
|
|
101
|
+
'tag.multimodal': '多模态',
|
|
102
|
+
'tag.textOnly': '纯文本',
|
|
103
|
+
'tag.modalityUnknown': '模态未知',
|
|
104
|
+
'tag.kind.image': '生图',
|
|
105
|
+
'tag.kind.video': '生视频',
|
|
106
|
+
'tag.kind.embedding': '嵌入',
|
|
107
|
+
'tag.kind.rerank': '重排',
|
|
108
|
+
'tag.kind.audio': '音频',
|
|
109
|
+
'tag.kind.classifier': '审核',
|
|
110
|
+
'tag.kind.translation': '翻译',
|
|
111
|
+
'tag.kind.unknown': '模型',
|
|
112
|
+
'tag.kindTitle': '模型类型:{kind};类型来源:{source}',
|
|
113
|
+
'tag.kindImageTitle': '图像生成模型(输出图像);类型来源:{source}',
|
|
114
|
+
'tag.kindVideoTitle': '视频生成模型(输出视频);类型来源:{source}',
|
|
115
|
+
'tag.correction': '官方校正',
|
|
116
|
+
'tag.correctionTitle': '{note}\n来源:{source}',
|
|
117
|
+
'tag.resolution': '分辨率',
|
|
118
|
+
'tag.resolutionTitle': '模型名标注了 {value} 输出档位',
|
|
119
|
+
'tag.imageInput': '可改图',
|
|
120
|
+
'tag.imageInputTitle': '支持图像输入(图生图 / 参考图)',
|
|
121
|
+
'tag.reasoningTitle': '支持推理,可用档位:{levels}(来源:{source})',
|
|
122
|
+
'tag.reasoningCatalogTitle': '支持推理;适配器未公布档位,此结论来自{source}',
|
|
123
|
+
'tag.effortTitle': '可用档位:{levels};默认:{fallback}(来源:{source})',
|
|
124
|
+
'tag.effortTitleNoFallback': '可用档位:{levels}(来源:{source})',
|
|
125
|
+
'tag.contextTitle': '上下文窗口 {tokens} tokens;来源:{source}',
|
|
126
|
+
'tag.modalityTitle': '输入模态:{modalities};来源:{source}',
|
|
127
|
+
'tag.outputTitle': '输出模态:{modalities};来源:{source}',
|
|
128
|
+
'tag.unknownTitle': '适配器与中转站都没有公布这项信息',
|
|
129
|
+
'tag.infoErrorTitle': '模型能力读取失败:{message}',
|
|
130
|
+
'source.config': '适配器配置',
|
|
131
|
+
'source.adapter': '适配器',
|
|
132
|
+
'source.provider': '中转站模型列表',
|
|
133
|
+
'source.catalog': '内置模型目录',
|
|
134
|
+
'source.inferred': '模型名推断',
|
|
135
|
+
'source.official': '厂商官方文档',
|
|
136
|
+
'source.manual': '手动设置',
|
|
137
|
+
'source.unknown': '未知',
|
|
138
|
+
'effort.off': '关闭',
|
|
139
|
+
'effort.minimal': '极低',
|
|
140
|
+
'effort.low': '低',
|
|
141
|
+
'effort.medium': '中',
|
|
142
|
+
'effort.high': '高',
|
|
143
|
+
'effort.max': '最高',
|
|
144
|
+
'effort.xhigh': '极高',
|
|
145
|
+
'tag.manual': '手动',
|
|
146
|
+
'tag.manualTitle': '这项能力由你手动指定,优先于适配器、中转站与内置目录。',
|
|
147
|
+
'row.edit': '编辑能力',
|
|
148
|
+
'row.editTitle': '手动指定这个模型的类型、上下文、输入模态与思考强度',
|
|
149
|
+
'override.title': '手动能力设置',
|
|
150
|
+
'override.hint': '这里填写的项会覆盖自动探测结果;留空或选「自动」的项继续按原规则判断。',
|
|
151
|
+
'override.kind': '模型类型',
|
|
152
|
+
'override.kindAuto': '自动判断',
|
|
153
|
+
'override.context': '上下文窗口',
|
|
154
|
+
'override.contextPlaceholder': '留空 = 自动',
|
|
155
|
+
'override.contextInvalid': '上下文窗口必须是大于 0 的整数。',
|
|
156
|
+
'override.input': '输入模态',
|
|
157
|
+
'override.efforts': '思考强度档位',
|
|
158
|
+
'override.defaultEffort': '默认档位',
|
|
159
|
+
'override.defaultEffortAuto': '不指定',
|
|
160
|
+
'override.effortHint':
|
|
161
|
+
'勾选「不支持推理」会隐藏思考强度;勾选档位后,这些档位会直接显示为该模型的可用强度。',
|
|
162
|
+
'override.save': '保存',
|
|
163
|
+
'override.saving': '保存中…',
|
|
164
|
+
'override.reset': '恢复自动',
|
|
165
|
+
'override.cancel': '取消',
|
|
166
|
+
'override.failed': '保存失败:{message}',
|
|
167
|
+
'override.unavailable': 'Host 半仍不支持手动能力设置;重启 DSH 后可用。',
|
|
168
|
+
'override.provider': '同时写入模型提供商设置',
|
|
169
|
+
'override.providerHint':
|
|
170
|
+
'勾选后,输入模态、上下文窗口与思考强度会写进「设置 → 模型提供商」里这个模型的配置,其他界面也能看到;只在本页可表达的项(模型类型、默认档位、仅 off 的档位)仍留在本插件。',
|
|
171
|
+
'override.providerWritten': '已同时写入提供商设置:{fields}',
|
|
172
|
+
'override.providerSkipped': '已保存在本插件;提供商设置未写入({reason})。',
|
|
173
|
+
'override.resetTitle': '只删除本插件的覆盖;已写入提供商设置的值不会被清除。',
|
|
174
|
+
'override.field.input': '输入模态',
|
|
175
|
+
'override.field.contextWindow': '上下文窗口',
|
|
176
|
+
'override.field.reasoningEfforts': '思考强度',
|
|
177
|
+
'override.reason.nothing': '没有可写入的字段',
|
|
178
|
+
'override.reason.unsupported': '当前 Host 不支持写入提供商设置',
|
|
179
|
+
'override.reason.provider-missing': '提供商设置里找不到该提供商',
|
|
180
|
+
'override.reason.models-missing': '该提供商没有显式模型列表',
|
|
181
|
+
'override.reason.model-missing': '该提供商的模型列表里没有这个模型',
|
|
182
|
+
'override.reason.describe-failed': '读取提供商设置失败',
|
|
183
|
+
'override.reason.settings-failed': '设置服务拒绝了写入',
|
|
184
|
+
'override.reason.skipped': '未请求写入',
|
|
185
|
+
'page.manualNote':
|
|
186
|
+
'「手动」标签表示这项能力来自你在这个模型上的手动设置,优先于适配器、中转站与内置目录。',
|
|
187
|
+
'correction.glm-5.3-text-only':
|
|
188
|
+
'官方文档:GLM-5.3 仅支持文本模态;原生多模态的是 GLM-5.3-Flash / FlashX。',
|
|
189
|
+
'modality.text': '文本',
|
|
190
|
+
'modality.image': '图像',
|
|
191
|
+
'modality.audio': '音频',
|
|
192
|
+
'modality.video': '视频',
|
|
193
|
+
'modality.file': '文件',
|
|
194
|
+
'provider.account': 'DeepSeek 账号',
|
|
195
|
+
},
|
|
196
|
+
en: {
|
|
197
|
+
'nav': 'Model visibility',
|
|
198
|
+
'page.title': 'Model visibility',
|
|
199
|
+
'page.description':
|
|
200
|
+
'Models turned off here disappear from the chat composer model list. Changes apply immediately and can be turned back on at any time. Every model shows its real capabilities: adapter declarations first, then the relay model list, then a built-in catalog; image, video and other generation models are labelled separately.',
|
|
201
|
+
'page.summary': 'Showing {visible} of {total} models',
|
|
202
|
+
'page.hiddenSummary': '{count} hidden',
|
|
203
|
+
'page.kindSummary': '{count} image/video models',
|
|
204
|
+
'page.loading': 'Loading model list…',
|
|
205
|
+
'page.empty': 'No models available.',
|
|
206
|
+
'page.filterEmpty': 'No models match this filter.',
|
|
207
|
+
'page.error': 'Could not load the model list: {message}',
|
|
208
|
+
'page.saveFailed': 'Could not save this change. Try again.',
|
|
209
|
+
'page.refresh': 'Refresh',
|
|
210
|
+
'page.refreshing': 'Refreshing…',
|
|
211
|
+
'page.reload': 'Re-fetch capabilities',
|
|
212
|
+
'page.reloading': 'Re-fetching…',
|
|
213
|
+
'page.reloadTitle':
|
|
214
|
+
'Drops this plugin’s capability caches and any in-flight read, then asks the adapter and every relay again. Model switches are untouched; effort levels the adapter does not publish still cannot appear.',
|
|
215
|
+
'page.reloadUnavailable': 'The Host half does not support a hard re-fetch; restart DSH to enable it.',
|
|
216
|
+
'page.collapseGroups': 'Collapse all groups',
|
|
217
|
+
'page.expandGroups': 'Expand all groups',
|
|
218
|
+
'page.collapseGroupsTitle': 'Keep only group headers so other groups are easy to reach',
|
|
219
|
+
'page.expandGroupsTitle': 'Expand every collapsed group',
|
|
220
|
+
'page.showAll': 'Show all',
|
|
221
|
+
'page.legend':
|
|
222
|
+
'Tags: Reasoning = adjustable levels; Context = input window; Multimodal = accepts images. Solid tags come from the adapter, the relay or vendor documentation; dashed tags come from the built-in catalog or the model name. Image, video, embedding and other generation models show only their kind, never effort or text tags.',
|
|
223
|
+
'page.metaFallback':
|
|
224
|
+
'Model capability details are unavailable right now: context and modalities show as unknown, while effort levels still come from the shipped catalog.',
|
|
225
|
+
'page.hostStale':
|
|
226
|
+
'The Host half is still the previous version: the colours and switch are live, but image admission and vendor corrections take effect after DSH restarts (or the profile recomposes).',
|
|
227
|
+
'page.providerFailures': 'Some providers could not be read: {names}',
|
|
228
|
+
'page.probeSummary': 'Relay probe: {ok} model lists read, {caps} published capability fields',
|
|
229
|
+
'page.probeSummaryNone': 'Relay probe: no readable route configuration',
|
|
230
|
+
'page.catalogNote': 'Built-in catalog: pi-ai {version} snapshot ({date}), {models} models',
|
|
231
|
+
'page.applyInput': 'Let the composer admit images shown as supported',
|
|
232
|
+
'page.applyInputHint':
|
|
233
|
+
'When on, relay chat models labelled Multimodal advertise image input to the request path; when off, only the Provider input declaration counts. Relay (pi-ai) routes only — no configuration file is modified. Turn it off if a particular upstream still rejects images.',
|
|
234
|
+
'page.kindLegend':
|
|
235
|
+
'Colours separate model kinds: image blue · video green · embedding orange · audio purple · rerank red · moderation rose · translation grey. “Official correction” means the vendor documentation overrides a wrong catalog row.',
|
|
236
|
+
'page.filterLabel': 'Filter',
|
|
237
|
+
'page.searchPlaceholder': 'Search model name or ID',
|
|
238
|
+
'page.capabilityLabel': 'Capabilities',
|
|
239
|
+
'page.capability.multimodal': 'Multimodal',
|
|
240
|
+
'page.capability.reasoning': 'Reasoning',
|
|
241
|
+
'page.contextLabel': 'Context at least',
|
|
242
|
+
'page.context.any': 'Any',
|
|
243
|
+
'page.context.128k': '128K+',
|
|
244
|
+
'page.context.256k': '256K+',
|
|
245
|
+
'page.context.512k': '512K+',
|
|
246
|
+
'page.context.1m': '1M+',
|
|
247
|
+
'page.filter.all': 'All',
|
|
248
|
+
'page.filter.chat': 'Chat',
|
|
249
|
+
'page.filter.image': 'Image',
|
|
250
|
+
'page.filter.video': 'Video',
|
|
251
|
+
'page.filter.other': 'Other',
|
|
252
|
+
'group.showAll': 'Show all',
|
|
253
|
+
'group.hideAll': 'Hide all',
|
|
254
|
+
'group.showFiltered': 'Show filtered',
|
|
255
|
+
'group.hideFiltered': 'Hide filtered',
|
|
256
|
+
'group.collapseNamed': 'Collapse the {name} group',
|
|
257
|
+
'group.expandNamed': 'Expand the {name} group',
|
|
258
|
+
'group.capabilities': '{chat} chat · {image} image · {video} video · {other} other',
|
|
259
|
+
'group.probeOk': 'relay: {models} models · {caps} with capability fields',
|
|
260
|
+
'group.probeNoCaps': 'relay: {models} models · no capability fields',
|
|
261
|
+
'group.probeFailed': 'relay read failed',
|
|
262
|
+
'group.probeSkipped': 'relay not probed',
|
|
263
|
+
'tag.reasoning': 'Reasoning',
|
|
264
|
+
'tag.noReasoning': 'No reasoning',
|
|
265
|
+
'tag.reasoningUnknown': 'Reasoning unknown',
|
|
266
|
+
'tag.effort': 'Effort',
|
|
267
|
+
'tag.context': 'Context',
|
|
268
|
+
'tag.unknown': 'unknown',
|
|
269
|
+
'tag.multimodal': 'Multimodal',
|
|
270
|
+
'tag.textOnly': 'Text only',
|
|
271
|
+
'tag.modalityUnknown': 'Modality unknown',
|
|
272
|
+
'tag.kind.image': 'Image',
|
|
273
|
+
'tag.kind.video': 'Video',
|
|
274
|
+
'tag.kind.embedding': 'Embedding',
|
|
275
|
+
'tag.kind.rerank': 'Rerank',
|
|
276
|
+
'tag.kind.audio': 'Audio',
|
|
277
|
+
'tag.kind.classifier': 'Moderation',
|
|
278
|
+
'tag.kind.translation': 'Translation',
|
|
279
|
+
'tag.kind.unknown': 'Model',
|
|
280
|
+
'tag.kindTitle': 'Model kind: {kind}; kind source: {source}',
|
|
281
|
+
'tag.kindImageTitle': 'Image generation model (outputs images); kind source: {source}',
|
|
282
|
+
'tag.kindVideoTitle': 'Video generation model (outputs video); kind source: {source}',
|
|
283
|
+
'tag.correction': 'Official correction',
|
|
284
|
+
'tag.correctionTitle': '{note}\nSource: {source}',
|
|
285
|
+
'tag.resolution': 'Resolution',
|
|
286
|
+
'tag.resolutionTitle': 'The model name advertises a {value} output tier',
|
|
287
|
+
'tag.imageInput': 'Image edit',
|
|
288
|
+
'tag.imageInputTitle': 'Accepts image input (image-to-image / reference images)',
|
|
289
|
+
'tag.reasoningTitle': 'Reasoning supported; levels: {levels} (source: {source})',
|
|
290
|
+
'tag.reasoningCatalogTitle': 'Reasoning supported; the adapter publishes no levels, so this comes from {source}',
|
|
291
|
+
'tag.effortTitle': 'Available levels: {levels}; default: {fallback} (source: {source})',
|
|
292
|
+
'tag.effortTitleNoFallback': 'Available levels: {levels} (source: {source})',
|
|
293
|
+
'tag.contextTitle': 'Context window {tokens} tokens; source: {source}',
|
|
294
|
+
'tag.modalityTitle': 'Input modalities: {modalities}; source: {source}',
|
|
295
|
+
'tag.outputTitle': 'Output modalities: {modalities}; source: {source}',
|
|
296
|
+
'tag.unknownTitle': 'Neither the adapter nor the relay publishes this fact',
|
|
297
|
+
'tag.infoErrorTitle': 'Could not read model capabilities: {message}',
|
|
298
|
+
'source.config': 'adapter configuration',
|
|
299
|
+
'source.adapter': 'adapter',
|
|
300
|
+
'source.provider': 'relay model list',
|
|
301
|
+
'source.catalog': 'built-in model catalog',
|
|
302
|
+
'source.inferred': 'model name inference',
|
|
303
|
+
'source.official': 'vendor documentation',
|
|
304
|
+
'source.manual': 'manual override',
|
|
305
|
+
'source.unknown': 'unknown',
|
|
306
|
+
'effort.off': 'Off',
|
|
307
|
+
'effort.minimal': 'Minimal',
|
|
308
|
+
'effort.low': 'Low',
|
|
309
|
+
'effort.medium': 'Medium',
|
|
310
|
+
'effort.high': 'High',
|
|
311
|
+
'effort.max': 'Max',
|
|
312
|
+
'effort.xhigh': 'XHigh',
|
|
313
|
+
'tag.manual': 'Manual',
|
|
314
|
+
'tag.manualTitle':
|
|
315
|
+
'You set this capability by hand; it outranks the adapter, the relay and the built-in catalog.',
|
|
316
|
+
'row.edit': 'Edit',
|
|
317
|
+
'row.editTitle':
|
|
318
|
+
'Set this model’s kind, context window, input modalities and effort levels by hand',
|
|
319
|
+
'override.title': 'Manual capabilities',
|
|
320
|
+
'override.hint':
|
|
321
|
+
'Anything filled in here overrides automatic detection; blank fields and “Automatic” keep the normal rules.',
|
|
322
|
+
'override.kind': 'Model kind',
|
|
323
|
+
'override.kindAuto': 'Automatic',
|
|
324
|
+
'override.context': 'Context window',
|
|
325
|
+
'override.contextPlaceholder': 'Blank = automatic',
|
|
326
|
+
'override.contextInvalid': 'The context window must be a whole number greater than 0.',
|
|
327
|
+
'override.input': 'Input modalities',
|
|
328
|
+
'override.efforts': 'Effort levels',
|
|
329
|
+
'override.defaultEffort': 'Default level',
|
|
330
|
+
'override.defaultEffortAuto': 'Unspecified',
|
|
331
|
+
'override.effortHint':
|
|
332
|
+
'“No reasoning” hides effort levels; checked levels are shown as this model’s available effort levels.',
|
|
333
|
+
'override.save': 'Save',
|
|
334
|
+
'override.saving': 'Saving…',
|
|
335
|
+
'override.reset': 'Use automatic',
|
|
336
|
+
'override.cancel': 'Cancel',
|
|
337
|
+
'override.failed': 'Could not save: {message}',
|
|
338
|
+
'override.unavailable':
|
|
339
|
+
'The Host half does not support manual capabilities yet; restart DSH to enable it.',
|
|
340
|
+
'override.provider': 'Also write to the model provider settings',
|
|
341
|
+
'override.providerHint':
|
|
342
|
+
'When checked, input modalities, context window and effort levels are written into this model’s entry under Settings → Model providers, so other views see them too. Fields that only exist here (kind, default level, an off-only ladder) stay in this plugin.',
|
|
343
|
+
'override.providerWritten': 'Also written to the provider settings: {fields}',
|
|
344
|
+
'override.providerSkipped': 'Saved in this plugin; the provider settings were not written ({reason}).',
|
|
345
|
+
'override.resetTitle':
|
|
346
|
+
'Removes this plugin’s override only; values already written to the provider settings are kept.',
|
|
347
|
+
'override.field.input': 'input modalities',
|
|
348
|
+
'override.field.contextWindow': 'context window',
|
|
349
|
+
'override.field.reasoningEfforts': 'effort levels',
|
|
350
|
+
'override.reason.nothing': 'nothing to write',
|
|
351
|
+
'override.reason.unsupported': 'this Host cannot write provider settings',
|
|
352
|
+
'override.reason.provider-missing': 'the provider is missing from the settings document',
|
|
353
|
+
'override.reason.models-missing': 'the provider has no explicit model list',
|
|
354
|
+
'override.reason.model-missing': 'the model is missing from the provider model list',
|
|
355
|
+
'override.reason.describe-failed': 'the settings document could not be read',
|
|
356
|
+
'override.reason.settings-failed': 'the settings service rejected the write',
|
|
357
|
+
'override.reason.skipped': 'not requested',
|
|
358
|
+
'page.manualNote':
|
|
359
|
+
'A “Manual” tag means you set that capability on this model; it outranks the adapter, the relay and the built-in catalog.',
|
|
360
|
+
'correction.glm-5.3-text-only':
|
|
361
|
+
'Official docs: GLM-5.3 accepts text only; GLM-5.3-Flash / FlashX is the native multimodal variant.',
|
|
362
|
+
'modality.text': 'text',
|
|
363
|
+
'modality.image': 'image',
|
|
364
|
+
'modality.audio': 'audio',
|
|
365
|
+
'modality.video': 'video',
|
|
366
|
+
'modality.file': 'file',
|
|
367
|
+
'provider.account': 'DeepSeek Account',
|
|
368
|
+
},
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const CSS = `
|
|
372
|
+
.mvp_page{box-sizing:border-box;width:100%;height:100%;min-height:0;flex-direction:column;display:flex;overflow-y:auto;padding-bottom:24px}
|
|
373
|
+
.mvp_head{flex:none;flex-direction:column;gap:6px;padding:4px 0 14px;display:flex}
|
|
374
|
+
.mvp_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}
|
|
375
|
+
.mvp_desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}
|
|
376
|
+
.mvp_toolbar{border-bottom:.5px solid var(--dsw-alias-border-l2);flex:none;justify-content:space-between;align-items:center;gap:12px;padding:10px 0;display:flex}
|
|
377
|
+
.mvp_summary{color:var(--dsw-alias-label-secondary);flex:1;min-width:0;font-size:12px;line-height:18px}
|
|
378
|
+
.mvp_actions{flex:none;align-items:center;justify-content:flex-end;gap:8px;display:flex;flex-wrap:wrap}
|
|
379
|
+
.mvp_button{box-sizing:border-box;height:28px;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);padding:0 10px;font-family:inherit;font-size:12px;line-height:18px}
|
|
380
|
+
.mvp_button--accent{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 55%, transparent)}
|
|
381
|
+
.mvp_button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
382
|
+
.mvp_button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
383
|
+
.mvp_button:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
384
|
+
.mvp_group{flex:none;flex-direction:column;display:flex}
|
|
385
|
+
.mvp_groupHead{justify-content:space-between;align-items:center;gap:12px;padding:14px 0 6px;display:flex}
|
|
386
|
+
.mvp_groupTitle{color:var(--dsw-alias-label-primary);align-items:baseline;row-gap:2px;gap:8px;font-size:14px;font-weight:500;line-height:22px;display:flex;flex-wrap:wrap}
|
|
387
|
+
.mvp_groupName{min-width:0}
|
|
388
|
+
.mvp_collapse{box-sizing:border-box;flex:none;align-self:center;width:18px;height:18px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:4px;justify-content:center;align-items:center;padding:0;font-family:inherit;font-size:11px;line-height:18px;display:inline-flex}
|
|
389
|
+
.mvp_collapse:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
390
|
+
.mvp_collapse:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
391
|
+
.mvp_groupCount{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}
|
|
392
|
+
.mvp_row{border-bottom:.5px solid var(--dsw-alias-border-l1);justify-content:space-between;align-items:center;gap:16px;padding:10px 0;display:flex}
|
|
393
|
+
.mvp_row--gen{background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 4%, transparent)}
|
|
394
|
+
.mvp_row--kind-image{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-business-primary) 70%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 4%, transparent)}
|
|
395
|
+
.mvp_row--kind-video{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-success-primary) 70%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 4%, transparent)}
|
|
396
|
+
.mvp_row--kind-embedding{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-warn-primary) 70%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 4%, transparent)}
|
|
397
|
+
.mvp_row--kind-rerank{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-error-primary) 60%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 3%, transparent)}
|
|
398
|
+
.mvp_row--kind-audio{box-shadow:inset 2px 0 0 color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, var(--dsw-alias-state-error-primary)) 70%, transparent);background:transparent;background:color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, var(--dsw-alias-state-error-primary)) 4%, transparent)}
|
|
399
|
+
.mvp_row--kind-classifier{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-error-secondary) 60%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-error-secondary) 3%, transparent)}
|
|
400
|
+
.mvp_row--kind-translation{box-shadow:inset 2px 0 0 var(--dsw-alias-border-l2)}
|
|
401
|
+
.mvp_rowText{flex-direction:column;gap:2px;min-width:0;display:flex}
|
|
402
|
+
.mvp_rowTitle{color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px}
|
|
403
|
+
.mvp_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}
|
|
404
|
+
.mvp_error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;padding:6px 0}
|
|
405
|
+
.mvp_switch{position:relative;flex:none;box-sizing:border-box;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-border-l2);cursor:pointer;transition:background-color .15s ease}
|
|
406
|
+
.mvp_switch[aria-checked="true"]{background:var(--dsw-alias-bg-base)}
|
|
407
|
+
.mvp_switch:disabled{opacity:.45;cursor:default}
|
|
408
|
+
.mvp_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
409
|
+
.mvp_thumb{position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:var(--dsw-alias-label-secondary);transition:transform .15s ease,background-color .15s ease}
|
|
410
|
+
.mvp_switch[aria-checked="true"] .mvp_thumb{background:var(--dsw-alias-label-primary);transform:translateX(16px)}
|
|
411
|
+
.mvp_tags{flex-wrap:wrap;align-items:center;gap:4px;margin-top:4px;display:flex}
|
|
412
|
+
.mvp_tag{box-sizing:border-box;max-width:100%;height:18px;color:var(--dsw-alias-label-secondary);white-space:nowrap;background:0 0;border:.5px solid var(--dsw-alias-border-l2);border-radius:4px;align-items:center;padding:0 6px;font-size:11px;line-height:16px;display:inline-flex;overflow:hidden;text-overflow:ellipsis}
|
|
413
|
+
.mvp_tag--accent{color:var(--dsw-alias-brand-primary);background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, transparent)}
|
|
414
|
+
.mvp_tag--vision{color:var(--dsw-alias-state-business-primary);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-business-primary) 45%, transparent)}
|
|
415
|
+
.mvp_tag--kind{font-weight:500}
|
|
416
|
+
.mvp_tag--kind-image{color:var(--dsw-alias-state-business-primary);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 12%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-business-primary) 55%, transparent)}
|
|
417
|
+
.mvp_tag--kind-video{color:var(--dsw-alias-state-success-primary);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 55%, transparent)}
|
|
418
|
+
.mvp_tag--kind-embedding{color:var(--dsw-alias-state-warn-primary);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 55%, transparent)}
|
|
419
|
+
.mvp_tag--kind-rerank{color:var(--dsw-alias-state-error-primary);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 50%, transparent)}
|
|
420
|
+
.mvp_tag--kind-audio{color:color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, var(--dsw-alias-state-error-primary));background:transparent;background:color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, var(--dsw-alias-state-error-primary)) 12%, transparent);border-color:color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, var(--dsw-alias-state-error-primary)) 50%, transparent)}
|
|
421
|
+
.mvp_tag--kind-classifier{color:var(--dsw-alias-state-error-secondary);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-error-secondary) 12%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-error-secondary) 55%, transparent)}
|
|
422
|
+
.mvp_tag--kind-translation{color:var(--dsw-alias-label-tertiary);border-color:var(--dsw-alias-border-l2)}
|
|
423
|
+
.mvp_tag--kind-unknown{color:var(--dsw-alias-label-secondary);border-color:var(--dsw-alias-border-l2)}
|
|
424
|
+
.mvp_tag--inferred{border-style:dashed}
|
|
425
|
+
.mvp_tag--muted{color:var(--dsw-alias-label-tertiary);border-color:var(--dsw-alias-border-l1)}
|
|
426
|
+
.mvp_groupCap{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:400;line-height:16px}
|
|
427
|
+
.mvp_kindCount--image{color:var(--dsw-alias-state-business-primary)}
|
|
428
|
+
.mvp_kindCount--video{color:var(--dsw-alias-state-success-primary)}
|
|
429
|
+
.mvp_kindCount--other{color:var(--dsw-alias-state-warn-primary)}
|
|
430
|
+
.mvp_applyRow{flex:none;align-items:flex-start;gap:10px;margin:8px 0 2px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);padding:8px 10px;display:flex;background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 4%, transparent)}
|
|
431
|
+
.mvp_applyText{flex-direction:column;gap:2px;min-width:0;display:flex}
|
|
432
|
+
.mvp_applyTitle{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500;line-height:18px}
|
|
433
|
+
.mvp_applyHint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
|
|
434
|
+
.mvp_applyRow .mvp_switch{margin-top:1px}
|
|
435
|
+
.mvp_groupProbe{color:var(--dsw-alias-label-tertiary);border:.5px dashed var(--dsw-alias-border-l2);border-radius:4px;max-width:100%;white-space:nowrap;text-overflow:ellipsis;padding:0 5px;font-size:10px;line-height:15px;overflow:hidden}
|
|
436
|
+
.mvp_filters{align-items:center;gap:6px;padding:8px 0 2px;display:flex;flex-wrap:wrap}
|
|
437
|
+
.mvp_filterLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
|
|
438
|
+
.mvp_search{align-items:center;gap:6px;display:flex;min-width:220px;flex:1 1 220px}
|
|
439
|
+
.mvp_searchInput{box-sizing:border-box;width:100%;height:28px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);padding:0 8px;font-family:inherit;font-size:12px;line-height:18px}
|
|
440
|
+
.mvp_searchInput:focus-visible,.mvp_contextSelect:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
441
|
+
.mvp_context{align-items:center;gap:6px;display:flex}
|
|
442
|
+
.mvp_contextSelect{box-sizing:border-box;height:24px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-base);border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);padding:0 6px;font-family:inherit;font-size:11px;line-height:18px}
|
|
443
|
+
.mvp_filter{box-sizing:border-box;height:24px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:.5px solid var(--dsw-alias-border-l2);border-radius:999px;padding:0 9px;font-family:inherit;font-size:11px;line-height:18px}
|
|
444
|
+
.mvp_filter:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
445
|
+
.mvp_filter[aria-pressed="true"]{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 55%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent)}
|
|
446
|
+
.mvp_filter:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
447
|
+
.mvp_note{color:var(--dsw-alias-label-tertiary);flex:none;padding:6px 0 0;font-size:10px;line-height:15px}
|
|
448
|
+
.mvp_legend{color:var(--dsw-alias-label-tertiary);flex:none;padding:8px 0 2px;font-size:11px;line-height:16px}
|
|
449
|
+
.mvp_fallback{color:var(--dsw-alias-label-secondary);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 10%, transparent);border-radius:var(--dsw-radius-sm,6px);padding:6px 8px;font-size:11px;line-height:16px;margin-top:8px}
|
|
450
|
+
.mvp_rowActions{flex:none;align-items:center;gap:8px;display:flex}
|
|
451
|
+
.mvp_editButton{height:24px;padding:0 8px;font-size:11px;line-height:18px}
|
|
452
|
+
.mvp_editButton[aria-pressed="true"]{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 55%, transparent)}
|
|
453
|
+
.mvp_editor{box-sizing:border-box;border:.5px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, transparent);border-radius:var(--dsw-radius-sm,6px);background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 4%, transparent);margin:2px 0 12px;padding:10px 12px;flex-direction:column;gap:10px;display:flex}
|
|
454
|
+
.mvp_editorHead{flex-direction:column;gap:2px;display:flex}
|
|
455
|
+
.mvp_editorTitle{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500;line-height:18px}
|
|
456
|
+
.mvp_editorSub{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
|
|
457
|
+
.mvp_editorGrid{flex-wrap:wrap;align-items:flex-end;gap:10px 16px;display:flex}
|
|
458
|
+
.mvp_field{flex-direction:column;gap:4px;min-width:140px;display:flex}
|
|
459
|
+
.mvp_field--wide{flex:1 1 100%}
|
|
460
|
+
.mvp_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}
|
|
461
|
+
.mvp_select,.mvp_number{box-sizing:border-box;height:26px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);padding:0 8px;font-family:inherit;font-size:12px;line-height:18px}
|
|
462
|
+
.mvp_number{width:180px}
|
|
463
|
+
.mvp_select:focus-visible,.mvp_number:focus-visible,.mvp_chip:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
464
|
+
.mvp_chips{flex-wrap:wrap;align-items:center;gap:6px;display:flex}
|
|
465
|
+
.mvp_chip{box-sizing:border-box;height:24px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:.5px solid var(--dsw-alias-border-l2);border-radius:999px;padding:0 10px;font-family:inherit;font-size:11px;line-height:18px}
|
|
466
|
+
.mvp_chip:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
467
|
+
.mvp_chip[aria-pressed="true"]{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 55%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent)}
|
|
468
|
+
.mvp_chip:disabled{opacity:.45;cursor:default}
|
|
469
|
+
.mvp_editorFooter{justify-content:space-between;align-items:center;gap:12px;display:flex;flex-wrap:wrap}
|
|
470
|
+
.mvp_editorNote{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:15px;flex:1 1 220px;min-width:0}
|
|
471
|
+
.mvp_check{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
472
|
+
.mvp_check input{accent-color:var(--dsw-alias-brand-primary, #4c8dff);margin:0}
|
|
473
|
+
.mvp_editorActions{flex:none;align-items:center;gap:6px;display:flex;flex-wrap:wrap}
|
|
474
|
+
.mvp_editorError{color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px}
|
|
475
|
+
`
|
|
476
|
+
|
|
477
|
+
/* ---------------------------------------------------------------- data */
|
|
478
|
+
|
|
479
|
+
/** Opaque key; never parsed back, so ids containing "/" stay unambiguous. */
|
|
480
|
+
function keyOf(provider, model) {
|
|
481
|
+
return provider + '/' + model
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function hiddenSetOf(snapshot) {
|
|
485
|
+
const value = snapshot && snapshot.value
|
|
486
|
+
const list = value && Array.isArray(value[FIELD]) ? value[FIELD] : []
|
|
487
|
+
return new Set(list)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function providerLabel(group, t) {
|
|
491
|
+
return group.id === 'deepseek-account' ? t('provider.account') : group.name
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const COLLAPSE_KEY = 'dsh-model-visibility:collapsed-groups:v1'
|
|
495
|
+
|
|
496
|
+
/** Group ids whose model lists the page keeps collapsed. */
|
|
497
|
+
function readCollapsedGroups() {
|
|
498
|
+
try {
|
|
499
|
+
const raw = window.localStorage.getItem(COLLAPSE_KEY)
|
|
500
|
+
if (raw === null) return new Set()
|
|
501
|
+
const parsed = JSON.parse(raw)
|
|
502
|
+
return new Set(Array.isArray(parsed) ? parsed.filter((id) => typeof id === 'string') : [])
|
|
503
|
+
} catch (_error) {
|
|
504
|
+
return new Set()
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function persistCollapsedGroups(collapsed) {
|
|
509
|
+
try {
|
|
510
|
+
window.localStorage.setItem(COLLAPSE_KEY, JSON.stringify(Array.from(collapsed)))
|
|
511
|
+
} catch (_error) {
|
|
512
|
+
// Storage can be unavailable (private mode, quota); collapsing still
|
|
513
|
+
// works for this page session.
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function countModels(groups) {
|
|
518
|
+
let total = 0
|
|
519
|
+
for (const group of groups || []) total += (group.models || []).length
|
|
520
|
+
return total
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Human context size: 1048576 → "1M", 128000 → "128K". */
|
|
524
|
+
function formatContext(tokens) {
|
|
525
|
+
if (!Number.isFinite(tokens) || tokens <= 0) return null
|
|
526
|
+
if (tokens >= 1_000_000) return Math.round(tokens / 100_000) / 10 + 'M'
|
|
527
|
+
if (tokens >= 1_000) return Math.round(tokens / 100) / 10 + 'K'
|
|
528
|
+
return String(tokens)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Effort display names in catalog order. */
|
|
532
|
+
function effortNames(reasoning) {
|
|
533
|
+
const efforts = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts : []
|
|
534
|
+
const names = []
|
|
535
|
+
for (const effort of efforts) {
|
|
536
|
+
if (effort === null || typeof effort !== 'object') continue
|
|
537
|
+
const label = effort.name || effort.id
|
|
538
|
+
if (typeof label === 'string' && label !== '') names.push(label)
|
|
539
|
+
}
|
|
540
|
+
return names
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Display name of the default effort, or null when none is published. */
|
|
544
|
+
function defaultEffortName(reasoning) {
|
|
545
|
+
if (!reasoning || typeof reasoning.defaultEffort !== 'string') return null
|
|
546
|
+
const efforts = Array.isArray(reasoning.efforts) ? reasoning.efforts : []
|
|
547
|
+
const found = efforts.find((effort) => effort !== null && effort.id === reasoning.defaultEffort)
|
|
548
|
+
return found && typeof found.name === 'string' && found.name !== ''
|
|
549
|
+
? found.name
|
|
550
|
+
: reasoning.defaultEffort
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Localized name of one input modality. */
|
|
554
|
+
function modalityLabel(id, t) {
|
|
555
|
+
const key = 'modality.' + id
|
|
556
|
+
const label = t(key)
|
|
557
|
+
return label === key ? id : label
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Localized provenance label ("adapter", "relay model list", …). */
|
|
561
|
+
function sourceLabel(source, t) {
|
|
562
|
+
const key = 'source.' + (source || 'unknown')
|
|
563
|
+
const label = t(key)
|
|
564
|
+
return label === key ? t('source.unknown') : label
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Per-field provenance recorded by the Host. */
|
|
568
|
+
function sourceOf(model, field) {
|
|
569
|
+
const sources = model && model.sources
|
|
570
|
+
return sources && typeof sources[field] === 'string' ? sources[field] : 'unknown'
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Catalog and name heuristics are completed values, not declarations. */
|
|
574
|
+
function isInferredSource(source) {
|
|
575
|
+
return source === 'catalog' || source === 'inferred'
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Kind with a safe fallback for records from an older Host. */
|
|
579
|
+
function kindOf(model) {
|
|
580
|
+
return model && typeof model.kind === 'string' && model.kind !== '' ? model.kind : 'chat'
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Filter bucket: chat / image / video / other. */
|
|
584
|
+
function bucketOf(kind) {
|
|
585
|
+
if (kind === 'image') return 'image'
|
|
586
|
+
if (kind === 'video') return 'video'
|
|
587
|
+
if (kind === 'chat') return 'chat'
|
|
588
|
+
return 'other'
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function isGenerationKind(kind) {
|
|
592
|
+
return kind === 'image' || kind === 'video' || kind === 'audio'
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** Resolution tier advertised in the model id ("-4k", "2K"). */
|
|
596
|
+
function resolutionOf(model) {
|
|
597
|
+
const text = String((model && model.id) || '')
|
|
598
|
+
const match = /(?:^|[-_/])(4k|2k|8k|uhd|hd)(?:$|[-_/])/i.exec(text)
|
|
599
|
+
return match === null ? null : match[1].toUpperCase()
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** "文本 / 图像" for one modality array. */
|
|
603
|
+
function modalityNames(modalities, t) {
|
|
604
|
+
return (Array.isArray(modalities) ? modalities : []).map((id) => modalityLabel(id, t)).join(' / ')
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Input modalities the manual editor can declare, in display order. */
|
|
608
|
+
const OVERRIDE_MODALITIES = ['text', 'image', 'audio', 'video', 'file']
|
|
609
|
+
|
|
610
|
+
/** Effort levels the manual editor can declare, in display order. */
|
|
611
|
+
const OVERRIDE_EFFORTS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
|
612
|
+
|
|
613
|
+
/** Localized name of one effort id. */
|
|
614
|
+
function effortLabel(id, t) {
|
|
615
|
+
const key = 'effort.' + id
|
|
616
|
+
const label = t(key)
|
|
617
|
+
return label === key ? id : label
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** Identity of a manual override entry; same shape as the row key. */
|
|
621
|
+
function overrideKeyOf(entry) {
|
|
622
|
+
return entry ? keyOf(entry.provider, entry.model) : null
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Model fields whose provenance can be manual. */
|
|
626
|
+
function manualFieldsOf(model) {
|
|
627
|
+
const sources = model && model.sources ? model.sources : {}
|
|
628
|
+
return ['kind', 'reasoning', 'context', 'modalities'].filter(
|
|
629
|
+
(field) => sources[field] === 'manual',
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function manualTag(model, t) {
|
|
634
|
+
return manualFieldsOf(model).length === 0
|
|
635
|
+
? null
|
|
636
|
+
: {
|
|
637
|
+
key: 'manual',
|
|
638
|
+
tone: 'accent',
|
|
639
|
+
label: t('tag.manual'),
|
|
640
|
+
title: t('tag.manualTitle'),
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Tags for image / video / embedding / rerank / audio / moderation models. */
|
|
645
|
+
function generationTags(model, t) {
|
|
646
|
+
const kind = kindOf(model)
|
|
647
|
+
const kindSource = sourceOf(model, 'kind')
|
|
648
|
+
const kindLabel = t('tag.kind.' + kind) === 'tag.kind.' + kind ? t('tag.kind.unknown') : t('tag.kind.' + kind)
|
|
649
|
+
const titleKey = kind === 'image' ? 'tag.kindImageTitle' : kind === 'video' ? 'tag.kindVideoTitle' : 'tag.kindTitle'
|
|
650
|
+
const tags = [
|
|
651
|
+
{
|
|
652
|
+
key: 'kind',
|
|
653
|
+
tone: 'kind-' + kind,
|
|
654
|
+
label: kindLabel,
|
|
655
|
+
title: t(titleKey, { kind: kindLabel, source: sourceLabel(kindSource, t) }),
|
|
656
|
+
inferred: isInferredSource(kindSource),
|
|
657
|
+
},
|
|
658
|
+
]
|
|
659
|
+
const resolution = resolutionOf(model)
|
|
660
|
+
if (resolution !== null && (kind === 'image' || kind === 'video')) {
|
|
661
|
+
tags.push({
|
|
662
|
+
key: 'resolution',
|
|
663
|
+
tone: 'neutral',
|
|
664
|
+
label: resolution,
|
|
665
|
+
title: t('tag.resolutionTitle', { value: resolution }),
|
|
666
|
+
})
|
|
667
|
+
}
|
|
668
|
+
// Image editing capability is the one input fact worth showing on a
|
|
669
|
+
// generation row; it never turns into the chat "multimodal" tag.
|
|
670
|
+
if (kind === 'image' && Array.isArray(model.inputModalities) && model.inputModalities.includes('image')) {
|
|
671
|
+
tags.push({
|
|
672
|
+
key: 'image-input',
|
|
673
|
+
tone: 'neutral',
|
|
674
|
+
label: t('tag.imageInput'),
|
|
675
|
+
title: t('tag.imageInputTitle'),
|
|
676
|
+
})
|
|
677
|
+
}
|
|
678
|
+
const manual = manualTag(model, t)
|
|
679
|
+
if (manual !== null) tags.push(manual)
|
|
680
|
+
return tags
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Per-model capability tags. The same model may know only part of its
|
|
685
|
+
* capabilities: an adapter that cannot answer produces `infoError`, and the
|
|
686
|
+
* unknown facts are shown as such instead of being guessed. Each tag knows
|
|
687
|
+
* whether its value is a declaration (solid border) or a completion from the
|
|
688
|
+
* built-in catalog / name heuristics (dashed border).
|
|
689
|
+
*/
|
|
690
|
+
function capabilityTags(model, t) {
|
|
691
|
+
const kind = kindOf(model)
|
|
692
|
+
if (kind !== 'chat') return generationTags(model, t)
|
|
693
|
+
|
|
694
|
+
const tags = []
|
|
695
|
+
const reasoning = model.reasoning
|
|
696
|
+
const levels = effortNames(reasoning)
|
|
697
|
+
const reasoningSource = sourceOf(model, 'reasoning')
|
|
698
|
+
const errorTitle =
|
|
699
|
+
typeof model.infoError === 'string' && model.infoError !== ''
|
|
700
|
+
? t('tag.infoErrorTitle', { message: model.infoError })
|
|
701
|
+
: t('tag.unknownTitle')
|
|
702
|
+
|
|
703
|
+
if (levels.length > 0) {
|
|
704
|
+
tags.push({
|
|
705
|
+
key: 'reasoning',
|
|
706
|
+
tone: 'accent',
|
|
707
|
+
label: t('tag.reasoning'),
|
|
708
|
+
title: t('tag.reasoningTitle', {
|
|
709
|
+
levels: levels.join(' / '),
|
|
710
|
+
source: sourceLabel(reasoningSource, t),
|
|
711
|
+
}),
|
|
712
|
+
})
|
|
713
|
+
const fallback = defaultEffortName(reasoning)
|
|
714
|
+
tags.push({
|
|
715
|
+
key: 'effort',
|
|
716
|
+
tone: 'neutral',
|
|
717
|
+
label: t('tag.effort') + ' ' + levels.join('/'),
|
|
718
|
+
title:
|
|
719
|
+
fallback === null
|
|
720
|
+
? t('tag.effortTitleNoFallback', {
|
|
721
|
+
levels: levels.join(' / '),
|
|
722
|
+
source: sourceLabel(reasoningSource, t),
|
|
723
|
+
})
|
|
724
|
+
: t('tag.effortTitle', {
|
|
725
|
+
levels: levels.join(' / '),
|
|
726
|
+
fallback,
|
|
727
|
+
source: sourceLabel(reasoningSource, t),
|
|
728
|
+
}),
|
|
729
|
+
})
|
|
730
|
+
} else if (model.reasoningSupported === true) {
|
|
731
|
+
tags.push({
|
|
732
|
+
key: 'reasoning',
|
|
733
|
+
tone: 'accent',
|
|
734
|
+
label: t('tag.reasoning'),
|
|
735
|
+
title: t('tag.reasoningCatalogTitle', { source: sourceLabel(reasoningSource, t) }),
|
|
736
|
+
inferred: isInferredSource(reasoningSource),
|
|
737
|
+
})
|
|
738
|
+
} else if (model.reasoningSupported === false) {
|
|
739
|
+
tags.push({
|
|
740
|
+
key: 'reasoning',
|
|
741
|
+
tone: 'muted',
|
|
742
|
+
label: t('tag.noReasoning'),
|
|
743
|
+
title: sourceLabel(reasoningSource, t),
|
|
744
|
+
inferred: isInferredSource(reasoningSource),
|
|
745
|
+
})
|
|
746
|
+
} else {
|
|
747
|
+
tags.push({ key: 'reasoning', tone: 'muted', label: t('tag.reasoningUnknown'), title: errorTitle })
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const contextSource = sourceOf(model, 'context')
|
|
751
|
+
const context = formatContext(model.contextWindow)
|
|
752
|
+
tags.push({
|
|
753
|
+
key: 'context',
|
|
754
|
+
tone: context === null ? 'muted' : 'neutral',
|
|
755
|
+
label: t('tag.context') + ' ' + (context === null ? t('tag.unknown') : context),
|
|
756
|
+
title:
|
|
757
|
+
context === null
|
|
758
|
+
? errorTitle
|
|
759
|
+
: t('tag.contextTitle', {
|
|
760
|
+
tokens: Number(model.contextWindow).toLocaleString(),
|
|
761
|
+
source: sourceLabel(contextSource, t),
|
|
762
|
+
}),
|
|
763
|
+
inferred: context !== null && isInferredSource(contextSource),
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
const modalities = Array.isArray(model.inputModalities) ? model.inputModalities : null
|
|
767
|
+
const modalitySource = sourceOf(model, 'modalities')
|
|
768
|
+
if (modalities === null || modalities.length === 0) {
|
|
769
|
+
tags.push({ key: 'modality', tone: 'muted', label: t('tag.modalityUnknown'), title: errorTitle })
|
|
770
|
+
} else {
|
|
771
|
+
const vision = modalities.includes('image')
|
|
772
|
+
tags.push({
|
|
773
|
+
key: 'modality',
|
|
774
|
+
tone: vision ? 'vision' : 'neutral',
|
|
775
|
+
label: vision ? t('tag.multimodal') : t('tag.textOnly'),
|
|
776
|
+
title: t('tag.modalityTitle', {
|
|
777
|
+
modalities: modalityNames(modalities, t),
|
|
778
|
+
source: sourceLabel(modalitySource, t),
|
|
779
|
+
}),
|
|
780
|
+
inferred: isInferredSource(modalitySource),
|
|
781
|
+
})
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// A vendor-documented correction is the one non-dashed value that is not
|
|
785
|
+
// an adapter declaration; keep it visible so "why is this text-only?"
|
|
786
|
+
// has an answer next to the tag.
|
|
787
|
+
const correction = model.correction
|
|
788
|
+
if (correction !== null && typeof correction === 'object' && typeof correction.code === 'string') {
|
|
789
|
+
const noteKey = 'correction.' + correction.code
|
|
790
|
+
const localized = t(noteKey)
|
|
791
|
+
const note = localized === noteKey
|
|
792
|
+
? typeof correction.note === 'string' && correction.note !== ''
|
|
793
|
+
? correction.note
|
|
794
|
+
: correction.code
|
|
795
|
+
: localized
|
|
796
|
+
tags.push({
|
|
797
|
+
key: 'correction',
|
|
798
|
+
tone: 'muted',
|
|
799
|
+
label: t('tag.correction'),
|
|
800
|
+
title: t('tag.correctionTitle', {
|
|
801
|
+
note,
|
|
802
|
+
source: typeof correction.source === 'string' && correction.source !== '' ? correction.source : '—',
|
|
803
|
+
}),
|
|
804
|
+
})
|
|
805
|
+
}
|
|
806
|
+
const manual = manualTag(model, t)
|
|
807
|
+
if (manual !== null) tags.push(manual)
|
|
808
|
+
return tags
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function subscribeOf(source) {
|
|
812
|
+
return (fn) => source.subscribe(fn)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function snapshotOf(source) {
|
|
816
|
+
return () => source.getSnapshot()
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function useSource(source) {
|
|
820
|
+
const subscribe = React.useCallback(subscribeOf(source), [source])
|
|
821
|
+
const getSnapshot = React.useCallback(snapshotOf(source), [source])
|
|
822
|
+
return React.useSyncExternalStore(subscribe, getSnapshot)
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function useHidden(form) {
|
|
826
|
+
const snapshot = useSource(form)
|
|
827
|
+
const hidden = React.useMemo(() => hiddenSetOf(snapshot), [snapshot])
|
|
828
|
+
return { hidden, snapshot }
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/** Host-generation catalog, shared by the settings page. */
|
|
832
|
+
function createCatalog(ctx) {
|
|
833
|
+
const listeners = new Set()
|
|
834
|
+
let state = {
|
|
835
|
+
status: 'idle',
|
|
836
|
+
groups: [],
|
|
837
|
+
failures: [],
|
|
838
|
+
providers: [],
|
|
839
|
+
catalog: null,
|
|
840
|
+
error: null,
|
|
841
|
+
detailsError: null,
|
|
842
|
+
source: null,
|
|
843
|
+
generatedAt: 0,
|
|
844
|
+
}
|
|
845
|
+
let inflight = null
|
|
846
|
+
// A hard reload starts a new run even while `inflight` is set; the token
|
|
847
|
+
// keeps the older run from publishing its now-stale result afterwards.
|
|
848
|
+
let generation = 0
|
|
849
|
+
|
|
850
|
+
const publish = (next) => {
|
|
851
|
+
state = next
|
|
852
|
+
for (const listener of listeners) listener()
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** Message of a failed Remote envelope. */
|
|
856
|
+
const failureMessage = (response) => {
|
|
857
|
+
const failure = response && response.error
|
|
858
|
+
return failure
|
|
859
|
+
? String(failure.code) + ': ' + String(failure.message)
|
|
860
|
+
: 'unknown error'
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Read the enriched catalog from the Host half. It carries the same
|
|
865
|
+
* groups as `session.modelCatalog` plus context windows and modalities.
|
|
866
|
+
*/
|
|
867
|
+
const fetchHostCatalog = async (fresh, reload) => {
|
|
868
|
+
const query = reload ? '?fresh=1&reload=1' : fresh ? '?fresh=1' : ''
|
|
869
|
+
const response = await fetch(API + '/models' + query, {
|
|
870
|
+
method: 'GET',
|
|
871
|
+
headers: { accept: 'application/json' },
|
|
872
|
+
credentials: 'same-origin',
|
|
873
|
+
cache: 'no-store',
|
|
874
|
+
redirect: 'error',
|
|
875
|
+
})
|
|
876
|
+
if (!response.ok) throw new Error('HTTP ' + response.status)
|
|
877
|
+
const body = await response.json()
|
|
878
|
+
const value = body && body.ok === true ? body.value : null
|
|
879
|
+
if (value === null || !Array.isArray(value.groups)) {
|
|
880
|
+
throw new Error('malformed capability catalog')
|
|
881
|
+
}
|
|
882
|
+
return value
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** Read the shipped catalog; the fallback when the route is unavailable. */
|
|
886
|
+
const fetchOfficialCatalog = async () => {
|
|
887
|
+
const response = await ctx.remote.session.modelCatalog()
|
|
888
|
+
if (!response || response.ok !== true) throw new Error(failureMessage(response))
|
|
889
|
+
const value = response.value || {}
|
|
890
|
+
return {
|
|
891
|
+
groups: Array.isArray(value.groups) ? value.groups : [],
|
|
892
|
+
failures: Array.isArray(value.failures) ? value.failures : [],
|
|
893
|
+
generatedAt: 0,
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const load = (options) => {
|
|
898
|
+
const fresh = options !== null && typeof options === 'object' && options.fresh === true
|
|
899
|
+
const reload = options !== null && typeof options === 'object' && options.reload === true
|
|
900
|
+
// A soft load joins whatever is already running. A hard reload has to
|
|
901
|
+
// start its own run, otherwise it would receive the very stale result
|
|
902
|
+
// it is meant to replace.
|
|
903
|
+
if (inflight !== null && !reload) return inflight
|
|
904
|
+
if (state.status !== 'ready') {
|
|
905
|
+
publish({ ...state, status: 'loading', error: null })
|
|
906
|
+
}
|
|
907
|
+
const token = ++generation
|
|
908
|
+
const run = (async () => {
|
|
909
|
+
try {
|
|
910
|
+
const value = await fetchHostCatalog(fresh, reload)
|
|
911
|
+
if (token !== generation) return
|
|
912
|
+
publish({
|
|
913
|
+
status: 'ready',
|
|
914
|
+
groups: Array.isArray(value.groups) ? value.groups : [],
|
|
915
|
+
failures: Array.isArray(value.failures) ? value.failures : [],
|
|
916
|
+
providers: Array.isArray(value.providers) ? value.providers : [],
|
|
917
|
+
catalog: value.catalog && typeof value.catalog === 'object' ? value.catalog : null,
|
|
918
|
+
error: null,
|
|
919
|
+
detailsError: null,
|
|
920
|
+
source: 'host',
|
|
921
|
+
generatedAt: Number.isFinite(value.generatedAt) ? value.generatedAt : 0,
|
|
922
|
+
})
|
|
923
|
+
return
|
|
924
|
+
} catch (detailError) {
|
|
925
|
+
const detail = String(detailError && detailError.message ? detailError.message : detailError)
|
|
926
|
+
try {
|
|
927
|
+
const value = await fetchOfficialCatalog()
|
|
928
|
+
if (token !== generation) return
|
|
929
|
+
publish({
|
|
930
|
+
status: 'ready',
|
|
931
|
+
groups: value.groups,
|
|
932
|
+
failures: value.failures,
|
|
933
|
+
providers: [],
|
|
934
|
+
catalog: null,
|
|
935
|
+
error: null,
|
|
936
|
+
detailsError: detail,
|
|
937
|
+
source: 'catalog',
|
|
938
|
+
generatedAt: value.generatedAt,
|
|
939
|
+
})
|
|
940
|
+
} catch (error) {
|
|
941
|
+
if (token !== generation) return
|
|
942
|
+
publish({
|
|
943
|
+
...state,
|
|
944
|
+
status: 'error',
|
|
945
|
+
error: String(error && error.message ? error.message : error),
|
|
946
|
+
})
|
|
947
|
+
}
|
|
948
|
+
} finally {
|
|
949
|
+
if (inflight === run) inflight = null
|
|
950
|
+
}
|
|
951
|
+
})()
|
|
952
|
+
inflight = run
|
|
953
|
+
return run
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return {
|
|
957
|
+
subscribe(fn) {
|
|
958
|
+
listeners.add(fn)
|
|
959
|
+
return () => {
|
|
960
|
+
listeners.delete(fn)
|
|
961
|
+
}
|
|
962
|
+
},
|
|
963
|
+
getSnapshot() {
|
|
964
|
+
return state
|
|
965
|
+
},
|
|
966
|
+
load,
|
|
967
|
+
refresh() {
|
|
968
|
+
return load({ fresh: true })
|
|
969
|
+
},
|
|
970
|
+
reload() {
|
|
971
|
+
return load({ fresh: true, reload: true })
|
|
972
|
+
},
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Manual capability overrides, persisted by the Host half.
|
|
978
|
+
*
|
|
979
|
+
* The page only edits one model at a time; the store keeps the whole list so
|
|
980
|
+
* every row knows whether it already has an override. An older Host half
|
|
981
|
+
* answers 404 and the editor stays hidden behind the `overrides` feature
|
|
982
|
+
* flag.
|
|
983
|
+
*/
|
|
984
|
+
function createOverrides() {
|
|
985
|
+
const listeners = new Set()
|
|
986
|
+
let state = { status: 'idle', entries: [], available: null, error: null }
|
|
987
|
+
let inflight = null
|
|
988
|
+
|
|
989
|
+
const publish = (next) => {
|
|
990
|
+
state = next
|
|
991
|
+
for (const listener of listeners) listener()
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** Error text for a failed envelope or HTTP response. */
|
|
995
|
+
const failureOf = async (response) => {
|
|
996
|
+
let detail = ''
|
|
997
|
+
try {
|
|
998
|
+
const body = await response.json()
|
|
999
|
+
if (body && body.error) {
|
|
1000
|
+
detail = ' (' + String(body.error.code) + ': ' + String(body.error.message) + ')'
|
|
1001
|
+
}
|
|
1002
|
+
} catch (_error) {
|
|
1003
|
+
// A proxy error page is not JSON; the status code is still useful.
|
|
1004
|
+
}
|
|
1005
|
+
return new Error('HTTP ' + response.status + detail)
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const readEntries = async (response) => {
|
|
1009
|
+
if (response.status === 404) {
|
|
1010
|
+
publish({ status: 'ready', entries: [], available: false, error: null })
|
|
1011
|
+
return null
|
|
1012
|
+
}
|
|
1013
|
+
if (!response.ok) throw await failureOf(response)
|
|
1014
|
+
const body = await response.json()
|
|
1015
|
+
const value = body && body.ok === true ? body.value : null
|
|
1016
|
+
if (value === null || !Array.isArray(value.overrides)) {
|
|
1017
|
+
throw new Error('malformed override list')
|
|
1018
|
+
}
|
|
1019
|
+
return value.overrides
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const load = () => {
|
|
1023
|
+
if (inflight !== null) return inflight
|
|
1024
|
+
if (state.status !== 'ready') publish({ ...state, status: 'loading', error: null })
|
|
1025
|
+
const run = (async () => {
|
|
1026
|
+
try {
|
|
1027
|
+
const response = await fetch(API + '/overrides', {
|
|
1028
|
+
method: 'GET',
|
|
1029
|
+
headers: { accept: 'application/json' },
|
|
1030
|
+
credentials: 'same-origin',
|
|
1031
|
+
cache: 'no-store',
|
|
1032
|
+
redirect: 'error',
|
|
1033
|
+
})
|
|
1034
|
+
const entries = await readEntries(response)
|
|
1035
|
+
if (entries !== null) {
|
|
1036
|
+
publish({ status: 'ready', entries, available: true, error: null })
|
|
1037
|
+
}
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
publish({
|
|
1040
|
+
...state,
|
|
1041
|
+
status: 'error',
|
|
1042
|
+
error: String(error && error.message ? error.message : error),
|
|
1043
|
+
})
|
|
1044
|
+
} finally {
|
|
1045
|
+
if (inflight === run) inflight = null
|
|
1046
|
+
}
|
|
1047
|
+
})()
|
|
1048
|
+
inflight = run
|
|
1049
|
+
return run
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/** POST one entry (or `{remove:true}`) and adopt the returned list. */
|
|
1053
|
+
const write = async (payload) => {
|
|
1054
|
+
const response = await fetch(API + '/overrides', {
|
|
1055
|
+
method: 'POST',
|
|
1056
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
1057
|
+
credentials: 'same-origin',
|
|
1058
|
+
cache: 'no-store',
|
|
1059
|
+
redirect: 'error',
|
|
1060
|
+
body: JSON.stringify(payload),
|
|
1061
|
+
})
|
|
1062
|
+
if (response.status === 404) {
|
|
1063
|
+
publish({ status: 'ready', entries: [], available: false, error: null })
|
|
1064
|
+
throw new Error('HTTP 404')
|
|
1065
|
+
}
|
|
1066
|
+
if (!response.ok) throw await failureOf(response)
|
|
1067
|
+
const body = await response.json()
|
|
1068
|
+
const value = body && body.ok === true ? body.value : null
|
|
1069
|
+
if (value === null || !Array.isArray(value.overrides)) {
|
|
1070
|
+
throw new Error('malformed override list')
|
|
1071
|
+
}
|
|
1072
|
+
publish({ status: 'ready', entries: value.overrides, available: true, error: null })
|
|
1073
|
+
return value
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return {
|
|
1077
|
+
subscribe(fn) {
|
|
1078
|
+
listeners.add(fn)
|
|
1079
|
+
return () => {
|
|
1080
|
+
listeners.delete(fn)
|
|
1081
|
+
}
|
|
1082
|
+
},
|
|
1083
|
+
getSnapshot() {
|
|
1084
|
+
return state
|
|
1085
|
+
},
|
|
1086
|
+
load,
|
|
1087
|
+
save(entry) {
|
|
1088
|
+
return write(entry)
|
|
1089
|
+
},
|
|
1090
|
+
remove(provider, model) {
|
|
1091
|
+
return write({ remove: true, provider, model })
|
|
1092
|
+
},
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/* ------------------------------------------------------ composer filter */
|
|
1097
|
+
|
|
1098
|
+
/** Hidden model keys as the plain array the Host stores. */
|
|
1099
|
+
function hiddenListOf(snapshot) {
|
|
1100
|
+
const value = snapshot && snapshot.value
|
|
1101
|
+
return value && Array.isArray(value[FIELD]) ? value[FIELD] : []
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Rewrite one directory store in place so its snapshots omit hidden models.
|
|
1106
|
+
*
|
|
1107
|
+
* The shipped composer seat may capture the directory store before this plugin
|
|
1108
|
+
* applies, so replacing the store object would never reach it; rewriting
|
|
1109
|
+
* getSnapshot/subscribe on the captured object does. The session's active
|
|
1110
|
+
* model stays listed even while hidden, because the shipped trigger
|
|
1111
|
+
* resolves its label from this same catalog.
|
|
1112
|
+
*/
|
|
1113
|
+
function patchDirectoryStore(store, form) {
|
|
1114
|
+
const readBase = store.getSnapshot.bind(store)
|
|
1115
|
+
const subscribeBase = store.subscribe.bind(store)
|
|
1116
|
+
const originalGet = store.getSnapshot
|
|
1117
|
+
const originalSubscribe = store.subscribe
|
|
1118
|
+
const listeners = new Set()
|
|
1119
|
+
let snapshot = null
|
|
1120
|
+
let lastBase
|
|
1121
|
+
let lastSignature
|
|
1122
|
+
|
|
1123
|
+
const build = (baseSnapshot) => {
|
|
1124
|
+
const hidden = new Set(hiddenListOf(form.getSnapshot()))
|
|
1125
|
+
const activeSelection = baseSnapshot.current
|
|
1126
|
+
const groups = []
|
|
1127
|
+
for (const group of baseSnapshot.groups || []) {
|
|
1128
|
+
const all = group.models || []
|
|
1129
|
+
const models = []
|
|
1130
|
+
for (const model of all) {
|
|
1131
|
+
const key = keyOf(group.id, model.id)
|
|
1132
|
+
const active =
|
|
1133
|
+
activeSelection !== null &&
|
|
1134
|
+
activeSelection !== undefined &&
|
|
1135
|
+
activeSelection.provider === group.id &&
|
|
1136
|
+
activeSelection.model === model.id
|
|
1137
|
+
if (!hidden.has(key) || active) models.push(model)
|
|
1138
|
+
}
|
|
1139
|
+
if (models.length === 0) continue
|
|
1140
|
+
groups.push(models.length === all.length ? group : { ...group, models })
|
|
1141
|
+
}
|
|
1142
|
+
return { ...baseSnapshot, groups }
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const current = () => {
|
|
1146
|
+
const baseSnapshot = readBase()
|
|
1147
|
+
const signature = hiddenListOf(form.getSnapshot()).join('\u0000')
|
|
1148
|
+
if (snapshot !== null && baseSnapshot === lastBase && signature === lastSignature) {
|
|
1149
|
+
return snapshot
|
|
1150
|
+
}
|
|
1151
|
+
lastBase = baseSnapshot
|
|
1152
|
+
lastSignature = signature
|
|
1153
|
+
snapshot = build(baseSnapshot)
|
|
1154
|
+
return snapshot
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const emit = () => {
|
|
1158
|
+
const previous = snapshot
|
|
1159
|
+
const next = current()
|
|
1160
|
+
if (previous === next) return
|
|
1161
|
+
for (const listener of Array.from(listeners)) listener()
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
Object.defineProperties(store, {
|
|
1165
|
+
__modelVisibilityPatched: { value: true, configurable: true },
|
|
1166
|
+
getSnapshot: { value: current, configurable: true, writable: true },
|
|
1167
|
+
subscribe: {
|
|
1168
|
+
value(listener) {
|
|
1169
|
+
listeners.add(listener)
|
|
1170
|
+
const stop = subscribeBase(listener)
|
|
1171
|
+
return () => {
|
|
1172
|
+
listeners.delete(listener)
|
|
1173
|
+
stop()
|
|
1174
|
+
}
|
|
1175
|
+
},
|
|
1176
|
+
configurable: true,
|
|
1177
|
+
writable: true,
|
|
1178
|
+
},
|
|
1179
|
+
})
|
|
1180
|
+
|
|
1181
|
+
return {
|
|
1182
|
+
emit,
|
|
1183
|
+
restore() {
|
|
1184
|
+
Object.defineProperties(store, {
|
|
1185
|
+
getSnapshot: { value: originalGet, configurable: true, writable: true },
|
|
1186
|
+
subscribe: { value: originalSubscribe, configurable: true, writable: true },
|
|
1187
|
+
})
|
|
1188
|
+
delete store.__modelVisibilityPatched
|
|
1189
|
+
listeners.clear()
|
|
1190
|
+
},
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Patch every model directory — the ones already live when the plugin
|
|
1196
|
+
* applies and every directory resolved later — so the shipped seat keeps
|
|
1197
|
+
* owning the trigger, the Model / Effort rows, and third-party menu
|
|
1198
|
+
* enhancements such as the reasoning-effort slider.
|
|
1199
|
+
*/
|
|
1200
|
+
function installComposerFilter(scope, form) {
|
|
1201
|
+
const models = scope.modelDirectories
|
|
1202
|
+
const original = models.directoryFor
|
|
1203
|
+
if (typeof original !== 'function') return
|
|
1204
|
+
const records = new Set()
|
|
1205
|
+
|
|
1206
|
+
const ensure = (directory) => {
|
|
1207
|
+
if (directory === undefined || directory === null) return directory
|
|
1208
|
+
const store = directory.store
|
|
1209
|
+
if (store === undefined || store === null || store.__modelVisibilityPatched === true) {
|
|
1210
|
+
return directory
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
records.add(patchDirectoryStore(store, form))
|
|
1214
|
+
} catch (_error) {
|
|
1215
|
+
// Fail open: an unwritable store simply keeps showing every model.
|
|
1216
|
+
}
|
|
1217
|
+
return directory
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
scope.effect(() => {
|
|
1221
|
+
const live = models.live && models.live.directories ? models.live.directories.values : null
|
|
1222
|
+
if (live instanceof Set) for (const directory of live) ensure(directory)
|
|
1223
|
+
|
|
1224
|
+
const stopForm = form.subscribe(() => {
|
|
1225
|
+
for (const record of records) record.emit()
|
|
1226
|
+
})
|
|
1227
|
+
|
|
1228
|
+
const wrapper = function directoryFor(sessionId) {
|
|
1229
|
+
return ensure(original.call(this, sessionId))
|
|
1230
|
+
}
|
|
1231
|
+
Object.defineProperty(models, 'directoryFor', {
|
|
1232
|
+
value: wrapper,
|
|
1233
|
+
configurable: true,
|
|
1234
|
+
writable: true,
|
|
1235
|
+
})
|
|
1236
|
+
|
|
1237
|
+
return () => {
|
|
1238
|
+
stopForm()
|
|
1239
|
+
for (const record of records) record.restore()
|
|
1240
|
+
records.clear()
|
|
1241
|
+
if (Object.getOwnPropertyDescriptor(models, 'directoryFor')?.value === wrapper) {
|
|
1242
|
+
delete models.directoryFor
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}, 'model-visibility: filtered model directory')
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/* ----------------------------------------------------- settings page */
|
|
1249
|
+
|
|
1250
|
+
/** Count models per filter bucket for one group. */
|
|
1251
|
+
function countKinds(models) {
|
|
1252
|
+
const counts = { chat: 0, image: 0, video: 0, other: 0 }
|
|
1253
|
+
for (const model of models || []) counts[bucketOf(kindOf(model))] += 1
|
|
1254
|
+
return counts
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/** Compact label + tooltip for one provider probe diagnostic. */
|
|
1258
|
+
function probeOf(state, group, t) {
|
|
1259
|
+
const list = Array.isArray(state.providers) ? state.providers : []
|
|
1260
|
+
const diagnostic = list.find((item) => item && item.id === group.id)
|
|
1261
|
+
if (diagnostic === undefined) return null
|
|
1262
|
+
const details = [
|
|
1263
|
+
diagnostic.host ? 'host: ' + diagnostic.host : null,
|
|
1264
|
+
diagnostic.status ? 'status: ' + diagnostic.status : null,
|
|
1265
|
+
diagnostic.message ? 'message: ' + diagnostic.message : null,
|
|
1266
|
+
diagnostic.pricing && diagnostic.pricing !== 'skipped' ? 'pricing: ' + diagnostic.pricing : null,
|
|
1267
|
+
]
|
|
1268
|
+
.filter(Boolean)
|
|
1269
|
+
.join('\n')
|
|
1270
|
+
if (diagnostic.status === 'ok') {
|
|
1271
|
+
return {
|
|
1272
|
+
label:
|
|
1273
|
+
Number(diagnostic.capabilities) > 0
|
|
1274
|
+
? t('group.probeOk', {
|
|
1275
|
+
models: diagnostic.models ?? 0,
|
|
1276
|
+
caps: diagnostic.capabilities,
|
|
1277
|
+
})
|
|
1278
|
+
: t('group.probeNoCaps', { models: diagnostic.models ?? 0 }),
|
|
1279
|
+
title: details,
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
if (diagnostic.status === 'failed' || diagnostic.status === 'unauthorized') {
|
|
1283
|
+
return { label: t('group.probeFailed'), title: details }
|
|
1284
|
+
}
|
|
1285
|
+
return { label: t('group.probeSkipped'), title: details }
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/** Class list for one capability tag. */
|
|
1289
|
+
function tagClassOf(tag) {
|
|
1290
|
+
const tone =
|
|
1291
|
+
tag.tone === 'accent'
|
|
1292
|
+
? ' mvp_tag--accent'
|
|
1293
|
+
: tag.tone === 'vision'
|
|
1294
|
+
? ' mvp_tag--vision'
|
|
1295
|
+
: tag.tone === 'kind'
|
|
1296
|
+
? ' mvp_tag--kind'
|
|
1297
|
+
: typeof tag.tone === 'string' && tag.tone.startsWith('kind-')
|
|
1298
|
+
? ' mvp_tag--kind mvp_tag--' + tag.tone
|
|
1299
|
+
: tag.tone === 'muted'
|
|
1300
|
+
? ' mvp_tag--muted'
|
|
1301
|
+
: ''
|
|
1302
|
+
return 'mvp_tag' + tone + (tag.inferred === true ? ' mvp_tag--inferred' : '')
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Per-model capability editor.
|
|
1307
|
+
*
|
|
1308
|
+
* Every field is optional: a blank field means "keep using automatic
|
|
1309
|
+
* detection", so a user can fix only the reasoning ladder or only the
|
|
1310
|
+
* context window without lying about the rest. Saving a fully blank form is
|
|
1311
|
+
* the same as "use automatic" and removes the entry on the Host side.
|
|
1312
|
+
*/
|
|
1313
|
+
/** Human sentence for the Host's provider write-back result, or null. */
|
|
1314
|
+
function providerWriteNote(result, t) {
|
|
1315
|
+
if (result === null || typeof result !== 'object') return null
|
|
1316
|
+
if (result.written === true) {
|
|
1317
|
+
const fields = (Array.isArray(result.fields) ? result.fields : [])
|
|
1318
|
+
.map((field) => t('override.field.' + field))
|
|
1319
|
+
.join('、')
|
|
1320
|
+
return t('override.providerWritten', { fields })
|
|
1321
|
+
}
|
|
1322
|
+
if (result.reason === 'nothing' || result.reason === 'skipped') return null
|
|
1323
|
+
const label = t('override.reason.' + String(result.reason))
|
|
1324
|
+
return t('override.providerSkipped', {
|
|
1325
|
+
reason: label === 'override.reason.' + String(result.reason) ? String(result.reason) : label,
|
|
1326
|
+
})
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function ModelOverrideEditor({ entry, name, t, overrides, onClose, onSaved }) {
|
|
1330
|
+
const [kind, setKind] = React.useState(entry.kind || '')
|
|
1331
|
+
const [context, setContext] = React.useState(
|
|
1332
|
+
entry.contextWindow === undefined ? '' : String(entry.contextWindow),
|
|
1333
|
+
)
|
|
1334
|
+
const [input, setInput] = React.useState(
|
|
1335
|
+
() => new Set(Array.isArray(entry.input) ? entry.input : []),
|
|
1336
|
+
)
|
|
1337
|
+
const [efforts, setEfforts] = React.useState(
|
|
1338
|
+
() => new Set((Array.isArray(entry.efforts) ? entry.efforts : []).map((item) => item.id)),
|
|
1339
|
+
)
|
|
1340
|
+
const [noReasoning, setNoReasoning] = React.useState(entry.reasoningSupported === false)
|
|
1341
|
+
const [defaultEffort, setDefaultEffort] = React.useState(
|
|
1342
|
+
typeof entry.defaultEffort === 'string' ? entry.defaultEffort : '',
|
|
1343
|
+
)
|
|
1344
|
+
const [busy, setBusy] = React.useState(false)
|
|
1345
|
+
const [error, setError] = React.useState(null)
|
|
1346
|
+
const [applyToProvider, setApplyToProvider] = React.useState(true)
|
|
1347
|
+
|
|
1348
|
+
const toggleIn = (setter, id) => {
|
|
1349
|
+
setter((previous) => {
|
|
1350
|
+
const next = new Set(previous)
|
|
1351
|
+
if (next.has(id)) next.delete(id)
|
|
1352
|
+
else next.add(id)
|
|
1353
|
+
return next
|
|
1354
|
+
})
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
const fail = (failure) => {
|
|
1358
|
+
setBusy(false)
|
|
1359
|
+
setError(
|
|
1360
|
+
t('override.failed', {
|
|
1361
|
+
message: String(failure && failure.message ? failure.message : failure),
|
|
1362
|
+
}),
|
|
1363
|
+
)
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const save = () => {
|
|
1367
|
+
const payload = { provider: entry.provider, model: entry.model }
|
|
1368
|
+
if (kind !== '') payload.kind = kind
|
|
1369
|
+
if (context.trim() !== '') {
|
|
1370
|
+
const value = Number(context)
|
|
1371
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1372
|
+
setError(t('override.contextInvalid'))
|
|
1373
|
+
return
|
|
1374
|
+
}
|
|
1375
|
+
payload.contextWindow = Math.round(value)
|
|
1376
|
+
}
|
|
1377
|
+
if (input.size > 0) {
|
|
1378
|
+
payload.input = OVERRIDE_MODALITIES.filter((id) => input.has(id))
|
|
1379
|
+
}
|
|
1380
|
+
if (noReasoning) {
|
|
1381
|
+
payload.reasoningSupported = false
|
|
1382
|
+
} else if (efforts.size > 0) {
|
|
1383
|
+
payload.efforts = OVERRIDE_EFFORTS.filter((id) => efforts.has(id)).map((id) => ({ id }))
|
|
1384
|
+
if (defaultEffort !== '' && efforts.has(defaultEffort)) {
|
|
1385
|
+
payload.defaultEffort = defaultEffort
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
payload.applyToProvider = applyToProvider
|
|
1389
|
+
setBusy(true)
|
|
1390
|
+
setError(null)
|
|
1391
|
+
Promise.resolve(overrides.save(payload)).then(
|
|
1392
|
+
(result) => {
|
|
1393
|
+
setBusy(false)
|
|
1394
|
+
if (typeof onSaved === 'function') onSaved(result)
|
|
1395
|
+
},
|
|
1396
|
+
fail,
|
|
1397
|
+
)
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const resetToAuto = () => {
|
|
1401
|
+
setBusy(true)
|
|
1402
|
+
setError(null)
|
|
1403
|
+
Promise.resolve(overrides.remove(entry.provider, entry.model)).then(
|
|
1404
|
+
() => {
|
|
1405
|
+
setBusy(false)
|
|
1406
|
+
if (typeof onSaved === 'function') onSaved()
|
|
1407
|
+
},
|
|
1408
|
+
fail,
|
|
1409
|
+
)
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
const kindOptions = [['', t('override.kindAuto')]].concat(
|
|
1413
|
+
['chat', 'image', 'video', 'embedding', 'rerank', 'audio', 'classifier', 'translation'].map(
|
|
1414
|
+
(id) => [id, id === 'chat' ? t('page.filter.chat') : t('tag.kind.' + id)],
|
|
1415
|
+
),
|
|
1416
|
+
)
|
|
1417
|
+
const defaultOptions = [['', t('override.defaultEffortAuto')]].concat(
|
|
1418
|
+
OVERRIDE_EFFORTS.filter((id) => efforts.has(id)).map((id) => [id, effortLabel(id, t)]),
|
|
1419
|
+
)
|
|
1420
|
+
const selectedDefault = efforts.has(defaultEffort) ? defaultEffort : ''
|
|
1421
|
+
|
|
1422
|
+
return h(
|
|
1423
|
+
'div',
|
|
1424
|
+
{ className: 'mvp_editor' },
|
|
1425
|
+
h(
|
|
1426
|
+
'div',
|
|
1427
|
+
{ className: 'mvp_editorHead' },
|
|
1428
|
+
h('div', { className: 'mvp_editorTitle' }, t('override.title') + ' · ' + name),
|
|
1429
|
+
h('div', { className: 'mvp_editorSub' }, t('override.hint')),
|
|
1430
|
+
),
|
|
1431
|
+
h(
|
|
1432
|
+
'div',
|
|
1433
|
+
{ className: 'mvp_editorGrid' },
|
|
1434
|
+
h(
|
|
1435
|
+
'label',
|
|
1436
|
+
{ className: 'mvp_field' },
|
|
1437
|
+
h('span', { className: 'mvp_fieldLabel' }, t('override.kind')),
|
|
1438
|
+
h(
|
|
1439
|
+
'select',
|
|
1440
|
+
{
|
|
1441
|
+
className: 'mvp_select',
|
|
1442
|
+
value: kind,
|
|
1443
|
+
'aria-label': t('override.kind'),
|
|
1444
|
+
onChange: (event) => setKind(event.target.value),
|
|
1445
|
+
},
|
|
1446
|
+
kindOptions.map(([value, label]) => h('option', { key: value, value }, label)),
|
|
1447
|
+
),
|
|
1448
|
+
),
|
|
1449
|
+
h(
|
|
1450
|
+
'label',
|
|
1451
|
+
{ className: 'mvp_field' },
|
|
1452
|
+
h('span', { className: 'mvp_fieldLabel' }, t('override.context')),
|
|
1453
|
+
h('input', {
|
|
1454
|
+
className: 'mvp_number',
|
|
1455
|
+
type: 'number',
|
|
1456
|
+
min: '1',
|
|
1457
|
+
step: '1',
|
|
1458
|
+
value: context,
|
|
1459
|
+
placeholder: t('override.contextPlaceholder'),
|
|
1460
|
+
'aria-label': t('override.context'),
|
|
1461
|
+
onInput: (event) => setContext(event.target.value),
|
|
1462
|
+
}),
|
|
1463
|
+
),
|
|
1464
|
+
h(
|
|
1465
|
+
'div',
|
|
1466
|
+
{ className: 'mvp_field mvp_field--wide' },
|
|
1467
|
+
h('span', { className: 'mvp_fieldLabel' }, t('override.input')),
|
|
1468
|
+
h(
|
|
1469
|
+
'div',
|
|
1470
|
+
{ className: 'mvp_chips' },
|
|
1471
|
+
OVERRIDE_MODALITIES.map((id) =>
|
|
1472
|
+
h(
|
|
1473
|
+
'button',
|
|
1474
|
+
{
|
|
1475
|
+
key: id,
|
|
1476
|
+
type: 'button',
|
|
1477
|
+
className: 'mvp_chip',
|
|
1478
|
+
'aria-pressed': input.has(id),
|
|
1479
|
+
disabled: busy,
|
|
1480
|
+
onClick: () => toggleIn(setInput, id),
|
|
1481
|
+
},
|
|
1482
|
+
modalityLabel(id, t),
|
|
1483
|
+
),
|
|
1484
|
+
),
|
|
1485
|
+
),
|
|
1486
|
+
),
|
|
1487
|
+
h(
|
|
1488
|
+
'div',
|
|
1489
|
+
{ className: 'mvp_field mvp_field--wide' },
|
|
1490
|
+
h('span', { className: 'mvp_fieldLabel' }, t('override.efforts')),
|
|
1491
|
+
h(
|
|
1492
|
+
'div',
|
|
1493
|
+
{ className: 'mvp_chips' },
|
|
1494
|
+
h(
|
|
1495
|
+
'button',
|
|
1496
|
+
{
|
|
1497
|
+
type: 'button',
|
|
1498
|
+
className: 'mvp_chip',
|
|
1499
|
+
'aria-pressed': noReasoning,
|
|
1500
|
+
disabled: busy,
|
|
1501
|
+
onClick: () => setNoReasoning((value) => !value),
|
|
1502
|
+
},
|
|
1503
|
+
t('tag.noReasoning'),
|
|
1504
|
+
),
|
|
1505
|
+
OVERRIDE_EFFORTS.map((id) =>
|
|
1506
|
+
h(
|
|
1507
|
+
'button',
|
|
1508
|
+
{
|
|
1509
|
+
key: id,
|
|
1510
|
+
type: 'button',
|
|
1511
|
+
className: 'mvp_chip',
|
|
1512
|
+
'aria-pressed': !noReasoning && efforts.has(id),
|
|
1513
|
+
disabled: busy || noReasoning,
|
|
1514
|
+
onClick: () => toggleIn(setEfforts, id),
|
|
1515
|
+
},
|
|
1516
|
+
effortLabel(id, t),
|
|
1517
|
+
),
|
|
1518
|
+
),
|
|
1519
|
+
),
|
|
1520
|
+
),
|
|
1521
|
+
!noReasoning && efforts.size > 0
|
|
1522
|
+
? h(
|
|
1523
|
+
'label',
|
|
1524
|
+
{ className: 'mvp_field' },
|
|
1525
|
+
h('span', { className: 'mvp_fieldLabel' }, t('override.defaultEffort')),
|
|
1526
|
+
h(
|
|
1527
|
+
'select',
|
|
1528
|
+
{
|
|
1529
|
+
className: 'mvp_select',
|
|
1530
|
+
value: selectedDefault,
|
|
1531
|
+
'aria-label': t('override.defaultEffort'),
|
|
1532
|
+
onChange: (event) => setDefaultEffort(event.target.value),
|
|
1533
|
+
},
|
|
1534
|
+
defaultOptions.map(([value, label]) => h('option', { key: value, value }, label)),
|
|
1535
|
+
),
|
|
1536
|
+
)
|
|
1537
|
+
: null,
|
|
1538
|
+
h(
|
|
1539
|
+
'div',
|
|
1540
|
+
{ className: 'mvp_field mvp_field--wide' },
|
|
1541
|
+
h(
|
|
1542
|
+
'label',
|
|
1543
|
+
{ className: 'mvp_check' },
|
|
1544
|
+
h('input', {
|
|
1545
|
+
type: 'checkbox',
|
|
1546
|
+
checked: applyToProvider,
|
|
1547
|
+
disabled: busy,
|
|
1548
|
+
onChange: (event) => setApplyToProvider(event.target.checked),
|
|
1549
|
+
}),
|
|
1550
|
+
h('span', null, t('override.provider')),
|
|
1551
|
+
),
|
|
1552
|
+
h('div', { className: 'mvp_editorNote' }, t('override.providerHint')),
|
|
1553
|
+
),
|
|
1554
|
+
),
|
|
1555
|
+
error !== null ? h('div', { className: 'mvp_editorError', role: 'alert' }, error) : null,
|
|
1556
|
+
h(
|
|
1557
|
+
'div',
|
|
1558
|
+
{ className: 'mvp_editorFooter' },
|
|
1559
|
+
h('div', { className: 'mvp_editorNote' }, t('override.effortHint')),
|
|
1560
|
+
h(
|
|
1561
|
+
'div',
|
|
1562
|
+
{ className: 'mvp_editorActions' },
|
|
1563
|
+
h(
|
|
1564
|
+
'button',
|
|
1565
|
+
{
|
|
1566
|
+
type: 'button',
|
|
1567
|
+
className: 'mvp_button',
|
|
1568
|
+
disabled: busy,
|
|
1569
|
+
title: t('override.resetTitle'),
|
|
1570
|
+
onClick: resetToAuto,
|
|
1571
|
+
},
|
|
1572
|
+
t('override.reset'),
|
|
1573
|
+
),
|
|
1574
|
+
h(
|
|
1575
|
+
'button',
|
|
1576
|
+
{ type: 'button', className: 'mvp_button', disabled: busy, onClick: onClose },
|
|
1577
|
+
t('override.cancel'),
|
|
1578
|
+
),
|
|
1579
|
+
h(
|
|
1580
|
+
'button',
|
|
1581
|
+
{
|
|
1582
|
+
type: 'button',
|
|
1583
|
+
className: 'mvp_button mvp_button--accent',
|
|
1584
|
+
disabled: busy,
|
|
1585
|
+
onClick: save,
|
|
1586
|
+
},
|
|
1587
|
+
t(busy ? 'override.saving' : 'override.save'),
|
|
1588
|
+
),
|
|
1589
|
+
),
|
|
1590
|
+
),
|
|
1591
|
+
)
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function makeSection(form, catalog, overrides) {
|
|
1595
|
+
return function ModelVisibilitySection({ t }) {
|
|
1596
|
+
const { hidden, snapshot } = useHidden(form)
|
|
1597
|
+
const state = useSource(catalog)
|
|
1598
|
+
const overrideState = useSource(overrides)
|
|
1599
|
+
const [busy, setBusy] = React.useState(false)
|
|
1600
|
+
const [error, setError] = React.useState(null)
|
|
1601
|
+
const [refreshing, setRefreshing] = React.useState(false)
|
|
1602
|
+
const [reloading, setReloading] = React.useState(false)
|
|
1603
|
+
const [collapsed, setCollapsed] = React.useState(readCollapsedGroups)
|
|
1604
|
+
const [editing, setEditing] = React.useState(null)
|
|
1605
|
+
const [providerNote, setProviderNote] = React.useState(null)
|
|
1606
|
+
|
|
1607
|
+
React.useEffect(() => {
|
|
1608
|
+
catalog.load()
|
|
1609
|
+
overrides.load()
|
|
1610
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1611
|
+
}, [])
|
|
1612
|
+
|
|
1613
|
+
const catalogMeta = state.catalog && typeof state.catalog === 'object' ? state.catalog : null
|
|
1614
|
+
const hostFeatures =
|
|
1615
|
+
catalogMeta && Array.isArray(catalogMeta.features) ? catalogMeta.features : []
|
|
1616
|
+
const canEdit = state.source === 'host' && hostFeatures.includes('overrides')
|
|
1617
|
+
const overrideEntries = new Map()
|
|
1618
|
+
for (const entry of overrideState.entries) {
|
|
1619
|
+
overrideEntries.set(overrideKeyOf(entry), entry)
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
const writable = snapshot.writable !== false
|
|
1623
|
+
const groups = state.groups
|
|
1624
|
+
const total = countModels(groups)
|
|
1625
|
+
const hiddenCount = groups.reduce(
|
|
1626
|
+
(count, group) =>
|
|
1627
|
+
count +
|
|
1628
|
+
(group.models || []).filter((model) => hidden.has(keyOf(group.id, model.id))).length,
|
|
1629
|
+
0,
|
|
1630
|
+
)
|
|
1631
|
+
const allKinds = countKinds(groups.flatMap((group) => group.models || []))
|
|
1632
|
+
const generationCount = allKinds.image + allKinds.video
|
|
1633
|
+
const [filter, setFilter] = React.useState('all')
|
|
1634
|
+
const [query, setQuery] = React.useState('')
|
|
1635
|
+
const [multimodalOnly, setMultimodalOnly] = React.useState(false)
|
|
1636
|
+
const [reasoningOnly, setReasoningOnly] = React.useState(false)
|
|
1637
|
+
const [minContext, setMinContext] = React.useState(0)
|
|
1638
|
+
const normalizedQuery = query.trim().toLowerCase()
|
|
1639
|
+
const filteredGroups = []
|
|
1640
|
+
for (const group of groups) {
|
|
1641
|
+
const models = (group.models || []).filter((model) => {
|
|
1642
|
+
if (filter !== 'all' && bucketOf(kindOf(model)) !== filter) return false
|
|
1643
|
+
if (normalizedQuery !== '') {
|
|
1644
|
+
const name = String(model.name || '').toLowerCase()
|
|
1645
|
+
const id = String(model.id || '').toLowerCase()
|
|
1646
|
+
if (!name.includes(normalizedQuery) && !id.includes(normalizedQuery)) return false
|
|
1647
|
+
}
|
|
1648
|
+
if (multimodalOnly && !(Array.isArray(model.inputModalities) && model.inputModalities.includes('image'))) {
|
|
1649
|
+
return false
|
|
1650
|
+
}
|
|
1651
|
+
if (reasoningOnly && model.reasoningSupported !== true) return false
|
|
1652
|
+
if (minContext > 0 && !(Number.isFinite(model.contextWindow) && model.contextWindow >= minContext)) {
|
|
1653
|
+
return false
|
|
1654
|
+
}
|
|
1655
|
+
return true
|
|
1656
|
+
})
|
|
1657
|
+
if (models.length > 0) filteredGroups.push({ ...group, models })
|
|
1658
|
+
}
|
|
1659
|
+
const disabled = busy || !writable
|
|
1660
|
+
|
|
1661
|
+
const writeField = (field, value) => {
|
|
1662
|
+
if (disabled) return
|
|
1663
|
+
setBusy(true)
|
|
1664
|
+
setError(null)
|
|
1665
|
+
Promise.resolve(form.set(field, value)).then(
|
|
1666
|
+
(accepted) => {
|
|
1667
|
+
if (accepted === false) setError(t('page.saveFailed'))
|
|
1668
|
+
},
|
|
1669
|
+
() => {
|
|
1670
|
+
setError(t('page.saveFailed'))
|
|
1671
|
+
},
|
|
1672
|
+
).finally(() => {
|
|
1673
|
+
setBusy(false)
|
|
1674
|
+
})
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
const commit = (next) => writeField(FIELD, Array.from(next))
|
|
1678
|
+
const applyInput = !(snapshot.value && snapshot.value.applyInput === false)
|
|
1679
|
+
const commitApply = (next) => writeField('applyInput', next)
|
|
1680
|
+
|
|
1681
|
+
const toggleModel = (group, model) => {
|
|
1682
|
+
const next = new Set(hidden)
|
|
1683
|
+
const key = keyOf(group.id, model.id)
|
|
1684
|
+
if (next.has(key)) next.delete(key)
|
|
1685
|
+
else next.add(key)
|
|
1686
|
+
commit(next)
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
const setGroup = (group, subset, visible) => {
|
|
1690
|
+
const next = new Set(hidden)
|
|
1691
|
+
for (const model of subset) {
|
|
1692
|
+
const key = keyOf(group.id, model.id)
|
|
1693
|
+
if (visible) next.delete(key)
|
|
1694
|
+
else next.add(key)
|
|
1695
|
+
}
|
|
1696
|
+
commit(next)
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
const runCatalogAction = (action, setActive) => {
|
|
1700
|
+
setActive(true)
|
|
1701
|
+
Promise.resolve()
|
|
1702
|
+
.then(action)
|
|
1703
|
+
.catch(() => {})
|
|
1704
|
+
.finally(() => setActive(false))
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
const refreshCatalog = () => {
|
|
1708
|
+
if (refreshing || reloading) return
|
|
1709
|
+
runCatalogAction(() => catalog.refresh(), setRefreshing)
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// A hard reload may supersede a soft refresh that is still running;
|
|
1713
|
+
// catalog.load() gives it a new run and discards the older result.
|
|
1714
|
+
const reloadCatalog = () => {
|
|
1715
|
+
if (busy || reloading) return
|
|
1716
|
+
runCatalogAction(() => catalog.reload(), setReloading)
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const toggleGroupCollapsed = (id) => {
|
|
1720
|
+
setCollapsed((previous) => {
|
|
1721
|
+
const next = new Set(previous)
|
|
1722
|
+
if (next.has(id)) next.delete(id)
|
|
1723
|
+
else next.add(id)
|
|
1724
|
+
persistCollapsedGroups(next)
|
|
1725
|
+
return next
|
|
1726
|
+
})
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const toggleAllCollapsed = () => {
|
|
1730
|
+
setCollapsed((previous) => {
|
|
1731
|
+
const collapsedNow = groups.some((group) => previous.has(group.id))
|
|
1732
|
+
const next = collapsedNow ? new Set() : new Set(groups.map((group) => group.id))
|
|
1733
|
+
persistCollapsedGroups(next)
|
|
1734
|
+
return next
|
|
1735
|
+
})
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
const rows = []
|
|
1739
|
+
for (const group of filteredGroups) {
|
|
1740
|
+
const allGroupModels =
|
|
1741
|
+
(groups.find((candidate) => candidate.id === group.id) || group).models || []
|
|
1742
|
+
const models = group.models || []
|
|
1743
|
+
const shown = allGroupModels.filter(
|
|
1744
|
+
(model) => !hidden.has(keyOf(group.id, model.id)),
|
|
1745
|
+
).length
|
|
1746
|
+
const allShown = models.every((model) => !hidden.has(keyOf(group.id, model.id)))
|
|
1747
|
+
const kinds = countKinds(allGroupModels)
|
|
1748
|
+
const kindParts = []
|
|
1749
|
+
if (kinds.chat > 0) kindParts.push(['chat', t('page.filter.chat') + ' ' + kinds.chat])
|
|
1750
|
+
if (kinds.image > 0) kindParts.push(['image', t('page.filter.image') + ' ' + kinds.image])
|
|
1751
|
+
if (kinds.video > 0) kindParts.push(['video', t('page.filter.video') + ' ' + kinds.video])
|
|
1752
|
+
if (kinds.other > 0) kindParts.push(['other', t('page.filter.other') + ' ' + kinds.other])
|
|
1753
|
+
const kindNodes = []
|
|
1754
|
+
for (const [index, [kindKey, label]] of kindParts.entries()) {
|
|
1755
|
+
if (index > 0) kindNodes.push(' · ')
|
|
1756
|
+
kindNodes.push(
|
|
1757
|
+
h(
|
|
1758
|
+
'span',
|
|
1759
|
+
{
|
|
1760
|
+
key: kindKey,
|
|
1761
|
+
className:
|
|
1762
|
+
'mvp_kindCount' +
|
|
1763
|
+
(kindKey === 'chat' ? '' : ' mvp_kindCount--' + kindKey),
|
|
1764
|
+
},
|
|
1765
|
+
label,
|
|
1766
|
+
),
|
|
1767
|
+
)
|
|
1768
|
+
}
|
|
1769
|
+
const groupName = providerLabel(group, t)
|
|
1770
|
+
const collapsedNow = collapsed.has(group.id)
|
|
1771
|
+
const collapseLabel = t(collapsedNow ? 'group.expandNamed' : 'group.collapseNamed', {
|
|
1772
|
+
name: groupName,
|
|
1773
|
+
})
|
|
1774
|
+
const probe = probeOf(state, group, t)
|
|
1775
|
+
rows.push(
|
|
1776
|
+
h(
|
|
1777
|
+
'div',
|
|
1778
|
+
{ key: group.id, className: 'mvp_group' },
|
|
1779
|
+
h(
|
|
1780
|
+
'div',
|
|
1781
|
+
{ className: 'mvp_groupHead' },
|
|
1782
|
+
h(
|
|
1783
|
+
'div',
|
|
1784
|
+
{ className: 'mvp_groupTitle' },
|
|
1785
|
+
h(
|
|
1786
|
+
'button',
|
|
1787
|
+
{
|
|
1788
|
+
type: 'button',
|
|
1789
|
+
className: 'mvp_collapse',
|
|
1790
|
+
'aria-expanded': !collapsedNow,
|
|
1791
|
+
'aria-label': collapseLabel,
|
|
1792
|
+
title: collapseLabel,
|
|
1793
|
+
onClick: () => toggleGroupCollapsed(group.id),
|
|
1794
|
+
},
|
|
1795
|
+
collapsedNow ? '▸' : '▾',
|
|
1796
|
+
),
|
|
1797
|
+
h('span', { className: 'mvp_groupName' }, groupName),
|
|
1798
|
+
h('span', { className: 'mvp_groupCount' }, shown + '/' + allGroupModels.length),
|
|
1799
|
+
kindParts.length > 0
|
|
1800
|
+
? h('span', { className: 'mvp_groupCap' }, kindNodes)
|
|
1801
|
+
: null,
|
|
1802
|
+
probe !== null
|
|
1803
|
+
? h('span', { className: 'mvp_groupProbe', title: probe.title || undefined }, probe.label)
|
|
1804
|
+
: null,
|
|
1805
|
+
),
|
|
1806
|
+
h(
|
|
1807
|
+
'button',
|
|
1808
|
+
{
|
|
1809
|
+
type: 'button',
|
|
1810
|
+
className: 'mvp_button',
|
|
1811
|
+
disabled: disabled,
|
|
1812
|
+
onClick: () => setGroup(group, models, !allShown),
|
|
1813
|
+
},
|
|
1814
|
+
allShown
|
|
1815
|
+
? filter === 'all'
|
|
1816
|
+
? t('group.hideAll')
|
|
1817
|
+
: t('group.hideFiltered')
|
|
1818
|
+
: filter === 'all'
|
|
1819
|
+
? t('group.showAll')
|
|
1820
|
+
: t('group.showFiltered'),
|
|
1821
|
+
),
|
|
1822
|
+
),
|
|
1823
|
+
collapsedNow
|
|
1824
|
+
? null
|
|
1825
|
+
: models.map((model) => {
|
|
1826
|
+
const rowKey = keyOf(group.id, model.id)
|
|
1827
|
+
const shownModel = !hidden.has(rowKey)
|
|
1828
|
+
const tags = capabilityTags(model, t)
|
|
1829
|
+
const kind = kindOf(model)
|
|
1830
|
+
const generation = isGenerationKind(kind)
|
|
1831
|
+
const kindTint =
|
|
1832
|
+
kind !== 'chat' &&
|
|
1833
|
+
['image', 'video', 'embedding', 'rerank', 'audio', 'classifier', 'translation'].includes(kind)
|
|
1834
|
+
? ' mvp_row--kind-' + kind
|
|
1835
|
+
: ''
|
|
1836
|
+
const row = h(
|
|
1837
|
+
'div',
|
|
1838
|
+
{
|
|
1839
|
+
className:
|
|
1840
|
+
'mvp_row' +
|
|
1841
|
+
(generation ? ' mvp_row--gen' : '') +
|
|
1842
|
+
kindTint,
|
|
1843
|
+
},
|
|
1844
|
+
h(
|
|
1845
|
+
'div',
|
|
1846
|
+
{ className: 'mvp_rowText' },
|
|
1847
|
+
h('div', { className: 'mvp_rowTitle' }, model.name),
|
|
1848
|
+
model.id === model.name
|
|
1849
|
+
? null
|
|
1850
|
+
: h('div', { className: 'mvp_rowMeta' }, model.id),
|
|
1851
|
+
h(
|
|
1852
|
+
'div',
|
|
1853
|
+
{ className: 'mvp_tags' },
|
|
1854
|
+
tags.map((tag) =>
|
|
1855
|
+
h(
|
|
1856
|
+
'span',
|
|
1857
|
+
{
|
|
1858
|
+
key: tag.key,
|
|
1859
|
+
className: tagClassOf(tag),
|
|
1860
|
+
title: tag.title || undefined,
|
|
1861
|
+
},
|
|
1862
|
+
tag.label,
|
|
1863
|
+
),
|
|
1864
|
+
),
|
|
1865
|
+
),
|
|
1866
|
+
),
|
|
1867
|
+
h(
|
|
1868
|
+
'div',
|
|
1869
|
+
{ className: 'mvp_rowActions' },
|
|
1870
|
+
canEdit
|
|
1871
|
+
? h(
|
|
1872
|
+
'button',
|
|
1873
|
+
{
|
|
1874
|
+
type: 'button',
|
|
1875
|
+
className: 'mvp_button mvp_editButton',
|
|
1876
|
+
'aria-pressed': editing === rowKey,
|
|
1877
|
+
'aria-label': t('row.edit') + ': ' + model.name,
|
|
1878
|
+
title: t('row.editTitle'),
|
|
1879
|
+
disabled: busy,
|
|
1880
|
+
onClick: () => setEditing(editing === rowKey ? null : rowKey),
|
|
1881
|
+
},
|
|
1882
|
+
t('row.edit'),
|
|
1883
|
+
)
|
|
1884
|
+
: null,
|
|
1885
|
+
h(
|
|
1886
|
+
'button',
|
|
1887
|
+
{
|
|
1888
|
+
type: 'button',
|
|
1889
|
+
role: 'switch',
|
|
1890
|
+
className: 'mvp_switch',
|
|
1891
|
+
'aria-checked': shownModel,
|
|
1892
|
+
'aria-label': model.name,
|
|
1893
|
+
disabled: disabled,
|
|
1894
|
+
onClick: () => toggleModel(group, model),
|
|
1895
|
+
},
|
|
1896
|
+
h('span', { className: 'mvp_thumb' }),
|
|
1897
|
+
),
|
|
1898
|
+
),
|
|
1899
|
+
)
|
|
1900
|
+
const editor =
|
|
1901
|
+
canEdit && editing === rowKey
|
|
1902
|
+
? h(ModelOverrideEditor, {
|
|
1903
|
+
key: 'editor',
|
|
1904
|
+
entry:
|
|
1905
|
+
overrideEntries.get(rowKey) || { provider: group.id, model: model.id },
|
|
1906
|
+
name: model.name,
|
|
1907
|
+
t,
|
|
1908
|
+
overrides,
|
|
1909
|
+
onClose: () => setEditing(null),
|
|
1910
|
+
onSaved: (result) => {
|
|
1911
|
+
setEditing(null)
|
|
1912
|
+
setProviderNote(providerWriteNote(result && result.provider, t))
|
|
1913
|
+
refreshCatalog()
|
|
1914
|
+
},
|
|
1915
|
+
})
|
|
1916
|
+
: null
|
|
1917
|
+
return h(React.Fragment, { key: model.id }, row, editor)
|
|
1918
|
+
}),
|
|
1919
|
+
),
|
|
1920
|
+
)
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
const loading = state.status === 'loading' || state.status === 'idle'
|
|
1924
|
+
const providerList = Array.isArray(state.providers) ? state.providers : []
|
|
1925
|
+
const probeOk = providerList.filter((provider) => provider && provider.status === 'ok').length
|
|
1926
|
+
const probeCaps = providerList.reduce(
|
|
1927
|
+
(count, provider) => count + (Number(provider && provider.capabilities) || 0),
|
|
1928
|
+
0,
|
|
1929
|
+
)
|
|
1930
|
+
const probeNote =
|
|
1931
|
+
state.source === 'host'
|
|
1932
|
+
? providerList.length > 0
|
|
1933
|
+
? t('page.probeSummary', { ok: probeOk, caps: probeCaps })
|
|
1934
|
+
: t('page.probeSummaryNone')
|
|
1935
|
+
: null
|
|
1936
|
+
// A page refresh can deliver this bundle while the Host half is still
|
|
1937
|
+
// the previous version; the switch would look live but do nothing.
|
|
1938
|
+
const hostStale = state.source === 'host' && !hostFeatures.includes('applyInput')
|
|
1939
|
+
// A hard reload is only meaningful when the Host half understands it.
|
|
1940
|
+
const canReload = state.source === 'host' && hostFeatures.includes('reload')
|
|
1941
|
+
const anyCollapsed = groups.some((group) => collapsed.has(group.id))
|
|
1942
|
+
const catalogDate =
|
|
1943
|
+
catalogMeta && typeof catalogMeta.generatedAt === 'string'
|
|
1944
|
+
? catalogMeta.generatedAt.slice(0, 10)
|
|
1945
|
+
: null
|
|
1946
|
+
const catalogNote =
|
|
1947
|
+
catalogMeta !== null
|
|
1948
|
+
? t('page.catalogNote', {
|
|
1949
|
+
version: catalogMeta.piAiVersion || '?',
|
|
1950
|
+
date: catalogDate || '?',
|
|
1951
|
+
models: catalogMeta.models ?? 0,
|
|
1952
|
+
})
|
|
1953
|
+
: null
|
|
1954
|
+
const filterOptions = [
|
|
1955
|
+
['all', t('page.filter.all'), total],
|
|
1956
|
+
['chat', t('page.filter.chat'), allKinds.chat],
|
|
1957
|
+
['image', t('page.filter.image'), allKinds.image],
|
|
1958
|
+
['video', t('page.filter.video'), allKinds.video],
|
|
1959
|
+
['other', t('page.filter.other'), allKinds.other],
|
|
1960
|
+
]
|
|
1961
|
+
const hasActiveFilters =
|
|
1962
|
+
filter !== 'all' || normalizedQuery !== '' || multimodalOnly || reasoningOnly || minContext > 0
|
|
1963
|
+
|
|
1964
|
+
return h(
|
|
1965
|
+
'div',
|
|
1966
|
+
{ className: 'mvp_page' },
|
|
1967
|
+
h(
|
|
1968
|
+
'div',
|
|
1969
|
+
{ className: 'mvp_head' },
|
|
1970
|
+
h('h2', { className: 'mvp_title' }, t('page.title')),
|
|
1971
|
+
h('p', { className: 'mvp_desc' }, t('page.description')),
|
|
1972
|
+
),
|
|
1973
|
+
h(
|
|
1974
|
+
'div',
|
|
1975
|
+
{ className: 'mvp_toolbar' },
|
|
1976
|
+
h(
|
|
1977
|
+
'div',
|
|
1978
|
+
{ className: 'mvp_summary' },
|
|
1979
|
+
total === 0
|
|
1980
|
+
? loading
|
|
1981
|
+
? t('page.loading')
|
|
1982
|
+
: t('page.empty')
|
|
1983
|
+
: t('page.summary', { visible: total - hiddenCount, total }) +
|
|
1984
|
+
(hiddenCount > 0 ? ' · ' + t('page.hiddenSummary', { count: hiddenCount }) : '') +
|
|
1985
|
+
(generationCount > 0 ? ' · ' + t('page.kindSummary', { count: generationCount }) : ''),
|
|
1986
|
+
),
|
|
1987
|
+
h(
|
|
1988
|
+
'div',
|
|
1989
|
+
{ className: 'mvp_actions' },
|
|
1990
|
+
h(
|
|
1991
|
+
'button',
|
|
1992
|
+
{
|
|
1993
|
+
type: 'button',
|
|
1994
|
+
className: 'mvp_button',
|
|
1995
|
+
disabled: disabled || hiddenCount === 0,
|
|
1996
|
+
onClick: () => commit(new Set()),
|
|
1997
|
+
},
|
|
1998
|
+
t('page.showAll'),
|
|
1999
|
+
),
|
|
2000
|
+
h(
|
|
2001
|
+
'button',
|
|
2002
|
+
{
|
|
2003
|
+
type: 'button',
|
|
2004
|
+
className: 'mvp_button',
|
|
2005
|
+
disabled: groups.length === 0,
|
|
2006
|
+
title: t(anyCollapsed ? 'page.expandGroupsTitle' : 'page.collapseGroupsTitle'),
|
|
2007
|
+
onClick: toggleAllCollapsed,
|
|
2008
|
+
},
|
|
2009
|
+
t(anyCollapsed ? 'page.expandGroups' : 'page.collapseGroups'),
|
|
2010
|
+
),
|
|
2011
|
+
h(
|
|
2012
|
+
'button',
|
|
2013
|
+
{
|
|
2014
|
+
type: 'button',
|
|
2015
|
+
className: 'mvp_button',
|
|
2016
|
+
disabled: busy || refreshing || reloading,
|
|
2017
|
+
onClick: refreshCatalog,
|
|
2018
|
+
},
|
|
2019
|
+
t(refreshing ? 'page.refreshing' : 'page.refresh'),
|
|
2020
|
+
),
|
|
2021
|
+
h(
|
|
2022
|
+
'button',
|
|
2023
|
+
{
|
|
2024
|
+
type: 'button',
|
|
2025
|
+
className: 'mvp_button mvp_button--accent',
|
|
2026
|
+
disabled: !canReload || busy || reloading,
|
|
2027
|
+
title: t(canReload ? 'page.reloadTitle' : 'page.reloadUnavailable'),
|
|
2028
|
+
onClick: reloadCatalog,
|
|
2029
|
+
},
|
|
2030
|
+
t(reloading ? 'page.reloading' : 'page.reload'),
|
|
2031
|
+
),
|
|
2032
|
+
),
|
|
2033
|
+
),
|
|
2034
|
+
h(
|
|
2035
|
+
'div',
|
|
2036
|
+
{ className: 'mvp_filters' },
|
|
2037
|
+
h('label', { className: 'mvp_search' },
|
|
2038
|
+
h('span', { className: 'mvp_filterLabel' }, t('page.searchPlaceholder')),
|
|
2039
|
+
h('input', {
|
|
2040
|
+
className: 'mvp_searchInput',
|
|
2041
|
+
type: 'search',
|
|
2042
|
+
value: query,
|
|
2043
|
+
placeholder: t('page.searchPlaceholder'),
|
|
2044
|
+
'aria-label': t('page.searchPlaceholder'),
|
|
2045
|
+
onInput: (event) => setQuery(event.target.value),
|
|
2046
|
+
}),
|
|
2047
|
+
),
|
|
2048
|
+
h('span', { className: 'mvp_filterLabel' }, t('page.filterLabel')),
|
|
2049
|
+
filterOptions.map(([value, label, count]) =>
|
|
2050
|
+
h(
|
|
2051
|
+
'button',
|
|
2052
|
+
{
|
|
2053
|
+
key: value,
|
|
2054
|
+
type: 'button',
|
|
2055
|
+
className: 'mvp_filter',
|
|
2056
|
+
'aria-pressed': filter === value,
|
|
2057
|
+
onClick: () => setFilter(value),
|
|
2058
|
+
},
|
|
2059
|
+
label + ' ' + count,
|
|
2060
|
+
),
|
|
2061
|
+
),
|
|
2062
|
+
h('span', { className: 'mvp_filterLabel' }, t('page.capabilityLabel')),
|
|
2063
|
+
h(
|
|
2064
|
+
'button',
|
|
2065
|
+
{
|
|
2066
|
+
type: 'button',
|
|
2067
|
+
className: 'mvp_filter',
|
|
2068
|
+
'aria-pressed': multimodalOnly,
|
|
2069
|
+
onClick: () => setMultimodalOnly(!multimodalOnly),
|
|
2070
|
+
},
|
|
2071
|
+
t('page.capability.multimodal'),
|
|
2072
|
+
),
|
|
2073
|
+
h(
|
|
2074
|
+
'button',
|
|
2075
|
+
{
|
|
2076
|
+
type: 'button',
|
|
2077
|
+
className: 'mvp_filter',
|
|
2078
|
+
'aria-pressed': reasoningOnly,
|
|
2079
|
+
onClick: () => setReasoningOnly(!reasoningOnly),
|
|
2080
|
+
},
|
|
2081
|
+
t('page.capability.reasoning'),
|
|
2082
|
+
),
|
|
2083
|
+
h('label', { className: 'mvp_context' },
|
|
2084
|
+
h('span', { className: 'mvp_filterLabel' }, t('page.contextLabel')),
|
|
2085
|
+
h(
|
|
2086
|
+
'select',
|
|
2087
|
+
{
|
|
2088
|
+
className: 'mvp_contextSelect',
|
|
2089
|
+
value: String(minContext),
|
|
2090
|
+
'aria-label': t('page.contextLabel'),
|
|
2091
|
+
onChange: (event) => setMinContext(Number(event.target.value)),
|
|
2092
|
+
},
|
|
2093
|
+
[
|
|
2094
|
+
[0, 'page.context.any'],
|
|
2095
|
+
[128000, 'page.context.128k'],
|
|
2096
|
+
[256000, 'page.context.256k'],
|
|
2097
|
+
[512000, 'page.context.512k'],
|
|
2098
|
+
[1000000, 'page.context.1m'],
|
|
2099
|
+
].map(([value, label]) => h('option', { key: value, value: String(value) }, t(label))),
|
|
2100
|
+
),
|
|
2101
|
+
),
|
|
2102
|
+
),
|
|
2103
|
+
h(
|
|
2104
|
+
'div',
|
|
2105
|
+
{ className: 'mvp_applyRow' },
|
|
2106
|
+
h(
|
|
2107
|
+
'button',
|
|
2108
|
+
{
|
|
2109
|
+
type: 'button',
|
|
2110
|
+
role: 'switch',
|
|
2111
|
+
className: 'mvp_switch',
|
|
2112
|
+
'aria-checked': applyInput,
|
|
2113
|
+
'aria-label': t('page.applyInput'),
|
|
2114
|
+
disabled: disabled,
|
|
2115
|
+
onClick: () => commitApply(!applyInput),
|
|
2116
|
+
},
|
|
2117
|
+
h('span', { className: 'mvp_thumb' }),
|
|
2118
|
+
),
|
|
2119
|
+
h(
|
|
2120
|
+
'div',
|
|
2121
|
+
{ className: 'mvp_applyText' },
|
|
2122
|
+
h('div', { className: 'mvp_applyTitle' }, t('page.applyInput')),
|
|
2123
|
+
h('div', { className: 'mvp_applyHint' }, t('page.applyInputHint')),
|
|
2124
|
+
),
|
|
2125
|
+
),
|
|
2126
|
+
h('div', { className: 'mvp_legend' }, t('page.legend')),
|
|
2127
|
+
h('div', { className: 'mvp_legend' }, t('page.kindLegend')),
|
|
2128
|
+
h('div', { className: 'mvp_legend' }, t('page.manualNote')),
|
|
2129
|
+
overrideState.available === false
|
|
2130
|
+
? h('div', { className: 'mvp_fallback' }, t('override.unavailable'))
|
|
2131
|
+
: null,
|
|
2132
|
+
state.detailsError !== null && state.source === 'catalog'
|
|
2133
|
+
? h('div', { className: 'mvp_fallback' }, t('page.metaFallback'))
|
|
2134
|
+
: null,
|
|
2135
|
+
hostStale ? h('div', { className: 'mvp_fallback' }, t('page.hostStale')) : null,
|
|
2136
|
+
state.failures.length > 0
|
|
2137
|
+
? h(
|
|
2138
|
+
'div',
|
|
2139
|
+
{
|
|
2140
|
+
className: 'mvp_fallback',
|
|
2141
|
+
title: state.failures
|
|
2142
|
+
.map((failure) => (failure.name || failure.id) + ': ' + failure.message)
|
|
2143
|
+
.join('\n'),
|
|
2144
|
+
},
|
|
2145
|
+
t('page.providerFailures', {
|
|
2146
|
+
names: state.failures
|
|
2147
|
+
.map((failure) => failure.name || failure.id)
|
|
2148
|
+
.join(' · '),
|
|
2149
|
+
}),
|
|
2150
|
+
)
|
|
2151
|
+
: null,
|
|
2152
|
+
error !== null ? h('div', { className: 'mvp_error', role: 'alert' }, error) : null,
|
|
2153
|
+
state.error !== null
|
|
2154
|
+
? h('div', { className: 'mvp_error', role: 'alert' }, t('page.error', { message: state.error }))
|
|
2155
|
+
: null,
|
|
2156
|
+
rows.length > 0 ? rows : null,
|
|
2157
|
+
total > 0 && hasActiveFilters && rows.length === 0
|
|
2158
|
+
? h('div', { className: 'mvp_note' }, t('page.filterEmpty'))
|
|
2159
|
+
: null,
|
|
2160
|
+
rows.length === 0 && !loading && state.error === null && total === 0
|
|
2161
|
+
? h('div', { className: 'mvp_error' }, t('page.empty'))
|
|
2162
|
+
: null,
|
|
2163
|
+
probeNote !== null ? h('div', { className: 'mvp_note' }, probeNote) : null,
|
|
2164
|
+
providerNote !== null ? h('div', { className: 'mvp_note' }, providerNote) : null,
|
|
2165
|
+
catalogNote !== null ? h('div', { className: 'mvp_note' }, catalogNote) : null,
|
|
2166
|
+
)
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/* --------------------------------------------------------------- plugin */
|
|
2171
|
+
|
|
2172
|
+
const inject = ['slots', 'locale', 'configForms', 'remote', 'remote.session']
|
|
2173
|
+
|
|
2174
|
+
function apply(ctx) {
|
|
2175
|
+
ctx.effect(() => ctx.locale.register(NS, DICT), 'model-visibility: dictionaries')
|
|
2176
|
+
|
|
2177
|
+
ctx.effect(() => {
|
|
2178
|
+
const tag = document.createElement('style')
|
|
2179
|
+
tag.dataset.plugin = 'dsh-model-visibility'
|
|
2180
|
+
tag.textContent = CSS
|
|
2181
|
+
document.head.appendChild(tag)
|
|
2182
|
+
return () => {
|
|
2183
|
+
tag.remove()
|
|
2184
|
+
}
|
|
2185
|
+
}, 'model-visibility: styles')
|
|
2186
|
+
|
|
2187
|
+
const form = ctx.configForms.get(ENTRY)
|
|
2188
|
+
const catalog = createCatalog(ctx)
|
|
2189
|
+
const overrides = createOverrides()
|
|
2190
|
+
|
|
2191
|
+
ctx.effect(() => {
|
|
2192
|
+
const disposers = []
|
|
2193
|
+
disposers.push(
|
|
2194
|
+
ctx.on('connection/reset', () => {
|
|
2195
|
+
catalog.refresh()
|
|
2196
|
+
overrides.load()
|
|
2197
|
+
}),
|
|
2198
|
+
)
|
|
2199
|
+
if (ctx.remote !== undefined && typeof ctx.remote.$on === 'function') {
|
|
2200
|
+
// A route change can alter capabilities (an adapter may report a new
|
|
2201
|
+
// effort ladder), so that read bypasses the Host-side cache.
|
|
2202
|
+
disposers.push(
|
|
2203
|
+
ctx.remote.$on('llm/adapters-updated', () => {
|
|
2204
|
+
catalog.refresh()
|
|
2205
|
+
}),
|
|
2206
|
+
)
|
|
2207
|
+
// Settings and credential writes can also change what the registry
|
|
2208
|
+
// knows, but hiding a model writes settings too — a normal read lets
|
|
2209
|
+
// the Host cache absorb that write instead of re-probing every model.
|
|
2210
|
+
for (const event of [
|
|
2211
|
+
'settings/document-updated',
|
|
2212
|
+
'credentials/record-updated',
|
|
2213
|
+
'credentials/reference-updated',
|
|
2214
|
+
]) {
|
|
2215
|
+
disposers.push(
|
|
2216
|
+
ctx.remote.$on(event, () => {
|
|
2217
|
+
catalog.load()
|
|
2218
|
+
}),
|
|
2219
|
+
)
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return () => {
|
|
2223
|
+
for (const dispose of disposers) {
|
|
2224
|
+
try {
|
|
2225
|
+
dispose()
|
|
2226
|
+
} catch (_error) {
|
|
2227
|
+
// A source may already be gone during teardown.
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}, 'model-visibility: catalog refresh')
|
|
2232
|
+
|
|
2233
|
+
const Section = makeSection(form, catalog, overrides)
|
|
2234
|
+
ctx.effect(
|
|
2235
|
+
() =>
|
|
2236
|
+
ctx.slots.inject('settings.section', () =>
|
|
2237
|
+
ctx.slots.register(
|
|
2238
|
+
{
|
|
2239
|
+
name: 'settings.section',
|
|
2240
|
+
id: SECTION_ID,
|
|
2241
|
+
order: 12,
|
|
2242
|
+
locale: NS,
|
|
2243
|
+
label: () => ctx.locale.bind(NS)('nav'),
|
|
2244
|
+
},
|
|
2245
|
+
Section,
|
|
2246
|
+
),
|
|
2247
|
+
),
|
|
2248
|
+
'model-visibility: settings section',
|
|
2249
|
+
)
|
|
2250
|
+
|
|
2251
|
+
ctx.inject(['modelDirectories'], (scope) => {
|
|
2252
|
+
installComposerFilter(scope, form)
|
|
2253
|
+
})
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
return { inject, apply }
|
|
2257
|
+
},
|
|
2258
|
+
})
|