dsh-openrouter-provider-advisor 0.1.0 → 0.1.2
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/CHANGELOG.md +56 -0
- package/docs/images/provider-panel.png +0 -0
- package/lib/client.js +464 -74
- package/lib/index.js +6 -4
- package/lib/types/client/i18n.d.ts +1 -1
- package/lib/types/client/provider-panel-store.d.ts +17 -1
- package/lib/types/client/provider-panel.d.ts +21 -0
- package/package.json +44 -10
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
## [0.1.2] - 2026-08-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Switch history with the provider pair and before/after throughput of each switch, persisted across sessions and offering one-click revert.
|
|
10
|
+
- Recommendation freshness line showing how long ago the ranking data was fetched.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The panel now remembers the last ranking strategy across close, reopen, and reload instead of resetting to Best overall.
|
|
15
|
+
- Applying a provider removes the plugin's own stale `@preset/` entries for the same OpenRouter model, so repeated switching no longer accumulates near-duplicate models. User-authored model entries are left untouched.
|
|
16
|
+
- Neutral panel colors follow the host `--dsw-alias-*` theme tokens, with the previous dark values as fallbacks.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- The provider dialog closes on Escape, traps Tab focus inside itself, and returns focus to the trigger button on close.
|
|
21
|
+
|
|
22
|
+
### Validation
|
|
23
|
+
|
|
24
|
+
- 79 automated tests passing.
|
|
25
|
+
- TypeScript typecheck and production build passing.
|
|
26
|
+
- `npm audit --omit=dev` reports zero vulnerabilities.
|
|
27
|
+
|
|
28
|
+
## [0.1.1] - 2026-08-24
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Explainable recommendation badges and per-dimension score breakdowns.
|
|
33
|
+
- Explicit input, output, and cache pricing in the provider list.
|
|
34
|
+
- Cache-heavy estimated savings compared with the current provider.
|
|
35
|
+
- One-click switching back to the previous provider.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Renamed the panel to “OpenRouter Provider Recommendations” / “OpenRouter 供应商推荐”.
|
|
40
|
+
- Polished refresh, close, provider-row, loading, success, and failure interactions.
|
|
41
|
+
- Replaced abrupt uptime safety bands with a continuous mild penalty curve, while retaining stronger penalties for genuinely unreliable endpoints.
|
|
42
|
+
- Updated the README provider-panel screenshot.
|
|
43
|
+
|
|
44
|
+
### Validation
|
|
45
|
+
|
|
46
|
+
- 75 automated tests passing.
|
|
47
|
+
- TypeScript typecheck and production build passing.
|
|
48
|
+
- `npm audit --omit=dev` reports zero vulnerabilities.
|
|
49
|
+
|
|
50
|
+
## [0.1.0] - 2026-08-24
|
|
51
|
+
|
|
52
|
+
- Initial public release.
|
|
53
|
+
|
|
54
|
+
[0.1.2]: https://github.com/xuanfengtechx/dsh-openrouter-provider-advisor/compare/v0.1.1...v0.1.2
|
|
55
|
+
[0.1.1]: https://github.com/xuanfengtechx/dsh-openrouter-provider-advisor/compare/v0.1.0...v0.1.1
|
|
56
|
+
[0.1.0]: https://github.com/xuanfengtechx/dsh-openrouter-provider-advisor/releases/tag/v0.1.0
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -32,7 +32,40 @@ async function responseJson(response) {
|
|
|
32
32
|
if (!response.ok) throw new Error(body.error ?? `HTTP ${response.status}`);
|
|
33
33
|
return body;
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
const STRATEGY_KEY = "openrouter-providers:strategy";
|
|
36
|
+
const HISTORY_KEY = "openrouter-providers:history";
|
|
37
|
+
const HISTORY_LIMIT = 20;
|
|
38
|
+
const KNOWN_STRATEGIES = new Set([
|
|
39
|
+
"balanced",
|
|
40
|
+
"price",
|
|
41
|
+
"speed",
|
|
42
|
+
"context"
|
|
43
|
+
]);
|
|
44
|
+
function defaultStorage() {
|
|
45
|
+
try {
|
|
46
|
+
return globalThis.localStorage;
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function readStrategy(storage) {
|
|
52
|
+
try {
|
|
53
|
+
const value = storage?.getItem(STRATEGY_KEY);
|
|
54
|
+
return value !== null && value !== void 0 && KNOWN_STRATEGIES.has(value) ? value : "balanced";
|
|
55
|
+
} catch {
|
|
56
|
+
return "balanced";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function readHistory(storage) {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(storage?.getItem(HISTORY_KEY) ?? "[]");
|
|
62
|
+
if (!Array.isArray(parsed)) return [];
|
|
63
|
+
return parsed.filter((item) => Boolean(item && typeof item === "object" && typeof item.at === "number" && typeof item.model === "string" && typeof item.toTag === "string")).slice(0, HISTORY_LIMIT);
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function createProviderPanelStore(fetchImpl = fetch, storage = defaultStorage()) {
|
|
36
69
|
let snapshot = {
|
|
37
70
|
open: false,
|
|
38
71
|
status: "idle",
|
|
@@ -40,9 +73,14 @@ function createProviderPanelStore(fetchImpl = fetch) {
|
|
|
40
73
|
data: null,
|
|
41
74
|
models: [],
|
|
42
75
|
selected: null,
|
|
43
|
-
strategy:
|
|
76
|
+
strategy: readStrategy(storage),
|
|
44
77
|
error: null,
|
|
45
|
-
credential: null
|
|
78
|
+
credential: null,
|
|
79
|
+
applyingTag: null,
|
|
80
|
+
successTag: null,
|
|
81
|
+
updatedAt: null,
|
|
82
|
+
previousTag: null,
|
|
83
|
+
history: readHistory(storage)
|
|
46
84
|
};
|
|
47
85
|
const listeners = /* @__PURE__ */ new Set();
|
|
48
86
|
let generation = 0;
|
|
@@ -90,14 +128,16 @@ function createProviderPanelStore(fetchImpl = fetch) {
|
|
|
90
128
|
publish({
|
|
91
129
|
status: "ready",
|
|
92
130
|
data,
|
|
93
|
-
error: null
|
|
131
|
+
error: null,
|
|
132
|
+
updatedAt: Date.now()
|
|
94
133
|
});
|
|
95
134
|
} catch (error) {
|
|
96
135
|
if (operation.id !== generation || operation.signal.aborted) return;
|
|
136
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
137
|
publish({
|
|
98
138
|
status: "error",
|
|
99
|
-
data: null,
|
|
100
|
-
error:
|
|
139
|
+
data: refresh ? snapshot.data : null,
|
|
140
|
+
error: message
|
|
101
141
|
});
|
|
102
142
|
throw error;
|
|
103
143
|
}
|
|
@@ -118,12 +158,15 @@ function createProviderPanelStore(fetchImpl = fetch) {
|
|
|
118
158
|
open: true,
|
|
119
159
|
status: "loading",
|
|
120
160
|
sessionId,
|
|
121
|
-
strategy: "balanced",
|
|
122
161
|
data: null,
|
|
123
162
|
models: [],
|
|
124
163
|
selected: null,
|
|
125
164
|
credential: null,
|
|
126
|
-
error: null
|
|
165
|
+
error: null,
|
|
166
|
+
applyingTag: null,
|
|
167
|
+
successTag: null,
|
|
168
|
+
updatedAt: null,
|
|
169
|
+
previousTag: null
|
|
127
170
|
});
|
|
128
171
|
try {
|
|
129
172
|
const catalog = await responseJson(await fetchImpl(`/api/openrouter-providers/models?sessionId=${encodeURIComponent(sessionId)}`, { signal: operation.signal }));
|
|
@@ -147,7 +190,7 @@ function createProviderPanelStore(fetchImpl = fetch) {
|
|
|
147
190
|
});
|
|
148
191
|
return;
|
|
149
192
|
}
|
|
150
|
-
await load(sessionId, false, selected
|
|
193
|
+
await load(sessionId, false, selected);
|
|
151
194
|
} catch (error) {
|
|
152
195
|
if (operation.id !== generation || operation.signal.aborted) return;
|
|
153
196
|
publish({
|
|
@@ -165,7 +208,6 @@ function createProviderPanelStore(fetchImpl = fetch) {
|
|
|
165
208
|
publish({
|
|
166
209
|
open: false,
|
|
167
210
|
status: snapshot.data ? "ready" : "idle",
|
|
168
|
-
strategy: "balanced",
|
|
169
211
|
error: null
|
|
170
212
|
});
|
|
171
213
|
},
|
|
@@ -183,16 +225,22 @@ function createProviderPanelStore(fetchImpl = fetch) {
|
|
|
183
225
|
await load(sessionId, false, selected);
|
|
184
226
|
},
|
|
185
227
|
async setStrategy(sessionId, strategy) {
|
|
228
|
+
try {
|
|
229
|
+
storage?.setItem(STRATEGY_KEY, strategy);
|
|
230
|
+
} catch {}
|
|
186
231
|
publish({ strategy });
|
|
187
232
|
await load(sessionId, false, snapshot.selected, strategy);
|
|
188
233
|
},
|
|
189
234
|
async apply(sessionId, tag) {
|
|
190
235
|
const operation = begin();
|
|
236
|
+
const previousTag = snapshot.data?.currentTag ?? null;
|
|
191
237
|
publish({
|
|
192
238
|
open: true,
|
|
193
239
|
status: "applying",
|
|
194
240
|
sessionId,
|
|
195
|
-
error: null
|
|
241
|
+
error: null,
|
|
242
|
+
applyingTag: tag,
|
|
243
|
+
successTag: null
|
|
196
244
|
});
|
|
197
245
|
try {
|
|
198
246
|
await responseJson(await fetchImpl("/api/openrouter-providers/apply", {
|
|
@@ -207,13 +255,52 @@ function createProviderPanelStore(fetchImpl = fetch) {
|
|
|
207
255
|
signal: operation.signal
|
|
208
256
|
}));
|
|
209
257
|
if (operation.id !== generation) return;
|
|
210
|
-
|
|
258
|
+
const markCurrent = (row) => ({
|
|
259
|
+
...row,
|
|
260
|
+
current: row.tag === tag
|
|
261
|
+
});
|
|
262
|
+
const data = snapshot.data ? {
|
|
263
|
+
...snapshot.data,
|
|
264
|
+
currentTag: tag,
|
|
265
|
+
recommended: snapshot.data.recommended.map(markCurrent),
|
|
266
|
+
rest: snapshot.data.rest.map(markCurrent)
|
|
267
|
+
} : null;
|
|
268
|
+
let history = snapshot.history;
|
|
269
|
+
if (snapshot.data && previousTag !== tag) {
|
|
270
|
+
const rows = [...snapshot.data.recommended, ...snapshot.data.rest];
|
|
271
|
+
const fromRow = previousTag === null ? void 0 : rows.find((row) => row.tag === previousTag);
|
|
272
|
+
const toRow = rows.find((row) => row.tag === tag);
|
|
273
|
+
history = [{
|
|
274
|
+
at: Date.now(),
|
|
275
|
+
model: snapshot.data.openrouterModel,
|
|
276
|
+
fromTag: previousTag,
|
|
277
|
+
fromName: fromRow?.providerName ?? null,
|
|
278
|
+
fromTps: fromRow?.tps ?? null,
|
|
279
|
+
toTag: tag,
|
|
280
|
+
toName: toRow?.providerName ?? tag,
|
|
281
|
+
toTps: toRow?.tps ?? null
|
|
282
|
+
}, ...snapshot.history].slice(0, HISTORY_LIMIT);
|
|
283
|
+
try {
|
|
284
|
+
storage?.setItem(HISTORY_KEY, JSON.stringify(history));
|
|
285
|
+
} catch {}
|
|
286
|
+
}
|
|
287
|
+
publish({
|
|
288
|
+
open: true,
|
|
289
|
+
status: "ready",
|
|
290
|
+
data,
|
|
291
|
+
history,
|
|
292
|
+
applyingTag: null,
|
|
293
|
+
successTag: tag,
|
|
294
|
+
previousTag: previousTag === tag ? snapshot.previousTag : previousTag,
|
|
295
|
+
error: null
|
|
296
|
+
});
|
|
211
297
|
} catch (error) {
|
|
212
298
|
if (operation.id !== generation || operation.signal.aborted) return;
|
|
213
299
|
publish({
|
|
214
300
|
open: true,
|
|
215
301
|
status: "error",
|
|
216
|
-
error: error instanceof Error ? error.message : String(error)
|
|
302
|
+
error: error instanceof Error ? error.message : String(error),
|
|
303
|
+
applyingTag: null
|
|
217
304
|
});
|
|
218
305
|
throw error;
|
|
219
306
|
}
|
|
@@ -232,7 +319,7 @@ function createProviderPanelStore(fetchImpl = fetch) {
|
|
|
232
319
|
//#region src/client/i18n.ts
|
|
233
320
|
const messages = {
|
|
234
321
|
"zh-CN": {
|
|
235
|
-
"panel.title": "OpenRouter
|
|
322
|
+
"panel.title": "OpenRouter 供应商推荐",
|
|
236
323
|
"panel.currentModelLoading": "正在读取当前模型…",
|
|
237
324
|
"panel.modelSelect": "选择 DSH 模型",
|
|
238
325
|
"panel.strategyGroup": "推荐策略",
|
|
@@ -244,17 +331,36 @@ const messages = {
|
|
|
244
331
|
"panel.group.recommended": "推荐 Top 5",
|
|
245
332
|
"panel.group.rest": "其他合格供应商",
|
|
246
333
|
"panel.column.provider": "供应商",
|
|
247
|
-
"panel.column.details": "
|
|
334
|
+
"panel.column.details": "性能与价格",
|
|
248
335
|
"panel.column.score": "分数",
|
|
249
336
|
"panel.current": "当前",
|
|
250
337
|
"panel.credentialMissing": "未检测到 OpenRouter 凭据“{ref}”。请前往 DSH 设置 → 模型完成配置;推荐、刷新和供应商切换暂不可用。",
|
|
251
338
|
"panel.switchHint": "点击供应商即可实时切换;当前会话与默认模型会立即更新。",
|
|
252
339
|
"panel.uptime": "可用率",
|
|
253
|
-
"panel.uptimeDelayHint": "可用率来自 OpenRouter 近 30
|
|
340
|
+
"panel.uptimeDelayHint": "可用率来自 OpenRouter 近 30 分钟统计并采用连续温和降权;故障与恢复可能存在检测延迟。",
|
|
341
|
+
"panel.refreshing": "正在刷新",
|
|
342
|
+
"panel.switching": "正在切换…",
|
|
343
|
+
"panel.switched": "已切换",
|
|
344
|
+
"panel.switchAction": "切换",
|
|
345
|
+
"panel.insight.quantization": "量化突出",
|
|
346
|
+
"panel.insight.speed": "速度突出",
|
|
347
|
+
"panel.insight.price": "价格突出",
|
|
348
|
+
"panel.insight.context": "上下文突出",
|
|
349
|
+
"panel.currentModel": "当前模型",
|
|
350
|
+
"panel.price.input": "输入",
|
|
351
|
+
"panel.price.output": "输出",
|
|
352
|
+
"panel.price.cache": "缓存",
|
|
353
|
+
"panel.savings": "预计节省 {percent}%",
|
|
354
|
+
"panel.undo": "切回上一个",
|
|
355
|
+
"panel.switchSuccess": "供应商已切换,可立即切回上一个选择。",
|
|
254
356
|
"panel.strategy.balanced": "综合最佳",
|
|
255
357
|
"panel.strategy.price": "价格优先",
|
|
256
358
|
"panel.strategy.speed": "速度优先",
|
|
257
359
|
"panel.strategy.context": "上下文优先",
|
|
360
|
+
"panel.updatedJustNow": "数据刚刚更新",
|
|
361
|
+
"panel.updatedMinutesAgo": "数据更新于 {minutes} 分钟前",
|
|
362
|
+
"panel.history.title": "切换历史",
|
|
363
|
+
"panel.history.revert": "切回",
|
|
258
364
|
"settings.title": "OpenRouter 供应商",
|
|
259
365
|
"settings.summary": "推荐数量、综合最佳权重与全局价格模型。",
|
|
260
366
|
"settings.action.weights": "应用综合权重",
|
|
@@ -280,7 +386,7 @@ const messages = {
|
|
|
280
386
|
"settings.group.weights.scope": "仅影响“综合最佳”推荐策略;调整一项时,其他未锁定项会按比例联动。",
|
|
281
387
|
"settings.group.priceBlend.title": "价格计算占比",
|
|
282
388
|
"settings.group.priceBlend.scope": "所有推荐策略共用。Code Agent 默认按 95% 以上缓存流量优化。",
|
|
283
|
-
"settings.group.advanced.title": "
|
|
389
|
+
"settings.group.advanced.title": "筛选与可靠性(阈值以下 5% 区间渐进降权)",
|
|
284
390
|
"settings.group.strategies.title": "查看其他策略权重",
|
|
285
391
|
"settings.group.strategies.scope": "以下策略使用内置权重;价格维度仍使用上方全局价格占比。",
|
|
286
392
|
"settings.dimension.quantization": "量化",
|
|
@@ -298,7 +404,7 @@ const messages = {
|
|
|
298
404
|
"settings.connection.guide": "请前往 DSH 设置 → 模型,为 OpenRouter provider 配置 API Key。插件不会读取或显示密钥内容。"
|
|
299
405
|
},
|
|
300
406
|
"en-US": {
|
|
301
|
-
"panel.title": "OpenRouter
|
|
407
|
+
"panel.title": "OpenRouter Provider Recommendations",
|
|
302
408
|
"panel.currentModelLoading": "Loading current model…",
|
|
303
409
|
"panel.modelSelect": "Select DSH model",
|
|
304
410
|
"panel.strategyGroup": "Ranking strategy",
|
|
@@ -310,17 +416,36 @@ const messages = {
|
|
|
310
416
|
"panel.group.recommended": "Top 5 Recommendations",
|
|
311
417
|
"panel.group.rest": "Other Eligible Providers",
|
|
312
418
|
"panel.column.provider": "Provider",
|
|
313
|
-
"panel.column.details": "
|
|
419
|
+
"panel.column.details": "Performance & price",
|
|
314
420
|
"panel.column.score": "Score",
|
|
315
421
|
"panel.current": "Current",
|
|
316
422
|
"panel.credentialMissing": "OpenRouter credential “{ref}” is not configured. Configure it in DSH Settings → Models; recommendations, refresh, and provider switching are disabled for now.",
|
|
317
423
|
"panel.switchHint": "Click a provider to switch immediately; the current session and default model update together.",
|
|
318
424
|
"panel.uptime": "uptime",
|
|
319
|
-
"panel.uptimeDelayHint": "Uptime
|
|
425
|
+
"panel.uptimeDelayHint": "Uptime uses a continuous mild penalty over OpenRouter's trailing 30-minute statistics; failures and recoveries may be detected with a delay.",
|
|
426
|
+
"panel.refreshing": "Refreshing",
|
|
427
|
+
"panel.switching": "Switching…",
|
|
428
|
+
"panel.switched": "Switched",
|
|
429
|
+
"panel.switchAction": "Switch",
|
|
430
|
+
"panel.insight.quantization": "Quality strength",
|
|
431
|
+
"panel.insight.speed": "Speed strength",
|
|
432
|
+
"panel.insight.price": "Price strength",
|
|
433
|
+
"panel.insight.context": "Context strength",
|
|
434
|
+
"panel.currentModel": "Current model",
|
|
435
|
+
"panel.price.input": "Input",
|
|
436
|
+
"panel.price.output": "Output",
|
|
437
|
+
"panel.price.cache": "Cache",
|
|
438
|
+
"panel.savings": "Est. {percent}% less",
|
|
439
|
+
"panel.undo": "Switch back",
|
|
440
|
+
"panel.switchSuccess": "Provider switched. You can immediately return to the previous choice.",
|
|
320
441
|
"panel.strategy.balanced": "Best overall",
|
|
321
442
|
"panel.strategy.price": "Price first",
|
|
322
443
|
"panel.strategy.speed": "Speed first",
|
|
323
444
|
"panel.strategy.context": "Context first",
|
|
445
|
+
"panel.updatedJustNow": "Updated just now",
|
|
446
|
+
"panel.updatedMinutesAgo": "Updated {minutes} min ago",
|
|
447
|
+
"panel.history.title": "Switch history",
|
|
448
|
+
"panel.history.revert": "Revert",
|
|
324
449
|
"settings.title": "OpenRouter Providers",
|
|
325
450
|
"settings.summary": "Recommended count, ranking weights, and price blend.",
|
|
326
451
|
"settings.action.weights": "Apply ranking weights",
|
|
@@ -346,7 +471,7 @@ const messages = {
|
|
|
346
471
|
"settings.group.weights.scope": "Affects only Best overall. Changing one item proportionally redistributes the other unlocked items.",
|
|
347
472
|
"settings.group.priceBlend.title": "Price calculation blend",
|
|
348
473
|
"settings.group.priceBlend.scope": "Shared by all ranking strategies. The Code Agent default is optimized for 95%+ cached traffic.",
|
|
349
|
-
"settings.group.advanced.title": "Filtering & reliability",
|
|
474
|
+
"settings.group.advanced.title": "Filtering & reliability (gradual penalty across 5% below threshold)",
|
|
350
475
|
"settings.group.strategies.title": "View other strategy weights",
|
|
351
476
|
"settings.group.strategies.scope": "These strategies use built-in weights; their price dimension still uses the global price blend above.",
|
|
352
477
|
"settings.dimension.quantization": "Quality",
|
|
@@ -399,6 +524,67 @@ function useUiI18n() {
|
|
|
399
524
|
|
|
400
525
|
//#endregion
|
|
401
526
|
//#region src/client/provider-panel.ts
|
|
527
|
+
const textPrimary = "var(--dsw-alias-label-primary, #f4f4f5)";
|
|
528
|
+
const textSecondary = "var(--dsw-alias-label-secondary, #d4d4d8)";
|
|
529
|
+
const textTertiary = "var(--dsw-alias-label-tertiary, #a1a1aa)";
|
|
530
|
+
const borderL2 = "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))";
|
|
531
|
+
const bgLayer1 = "var(--dsw-alias-bg-layer-1, #18181b)";
|
|
532
|
+
const bgLayer2 = "var(--dsw-alias-bg-layer-2, #27272a)";
|
|
533
|
+
const bgLayer3 = "var(--dsw-alias-bg-layer-3, rgba(255,255,255,.05))";
|
|
534
|
+
const actionButtonStyle = {
|
|
535
|
+
height: 36,
|
|
536
|
+
padding: "0 12px",
|
|
537
|
+
borderRadius: 8,
|
|
538
|
+
border: borderL2,
|
|
539
|
+
background: bgLayer3,
|
|
540
|
+
color: textPrimary,
|
|
541
|
+
cursor: "pointer"
|
|
542
|
+
};
|
|
543
|
+
const iconButtonStyle = {
|
|
544
|
+
...actionButtonStyle,
|
|
545
|
+
width: 36,
|
|
546
|
+
padding: 0,
|
|
547
|
+
fontSize: 20,
|
|
548
|
+
lineHeight: 1
|
|
549
|
+
};
|
|
550
|
+
function providerInsight(row, i18n = createI18n("zh-CN")) {
|
|
551
|
+
const entries = [
|
|
552
|
+
[
|
|
553
|
+
"quantization",
|
|
554
|
+
row.dimensions.quantization,
|
|
555
|
+
i18n.t("settings.dimension.quantization")
|
|
556
|
+
],
|
|
557
|
+
[
|
|
558
|
+
"speed",
|
|
559
|
+
row.dimensions.speed,
|
|
560
|
+
i18n.t("settings.dimension.speed")
|
|
561
|
+
],
|
|
562
|
+
[
|
|
563
|
+
"price",
|
|
564
|
+
row.dimensions.price,
|
|
565
|
+
i18n.t("settings.dimension.price")
|
|
566
|
+
],
|
|
567
|
+
[
|
|
568
|
+
"context",
|
|
569
|
+
row.dimensions.context,
|
|
570
|
+
i18n.t("settings.dimension.context")
|
|
571
|
+
]
|
|
572
|
+
];
|
|
573
|
+
const strongest = entries.reduce((best, item) => item[1] > best[1] ? item : best);
|
|
574
|
+
return {
|
|
575
|
+
badge: i18n.t(`panel.insight.${strongest[0]}`),
|
|
576
|
+
breakdown: entries.map(([, value, label]) => `${label} ${Math.round(value)}`).join(" · ")
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function providerPriceLabel(row, i18n = createI18n("zh-CN")) {
|
|
580
|
+
return `${i18n.t("panel.price.input")} $${row.price.input} · ${i18n.t("panel.price.output")} $${row.price.output} · ${i18n.t("panel.price.cache")} $${row.price.cache} / M tokens`;
|
|
581
|
+
}
|
|
582
|
+
function estimatedSavings(current, candidate) {
|
|
583
|
+
const cost = (price) => price.input * .02 + price.output * .08 + price.cache * .9;
|
|
584
|
+
const baseline = cost(current);
|
|
585
|
+
const saving = baseline > 0 ? Math.round((1 - cost(candidate) / baseline) * 100) : 0;
|
|
586
|
+
return saving > 0 ? saving : null;
|
|
587
|
+
}
|
|
402
588
|
const STRATEGY_OPTIONS = [
|
|
403
589
|
{
|
|
404
590
|
id: "balanced",
|
|
@@ -418,14 +604,21 @@ const STRATEGY_OPTIONS = [
|
|
|
418
604
|
}
|
|
419
605
|
];
|
|
420
606
|
function providerRows(data, i18n = createI18n("zh-CN")) {
|
|
607
|
+
const currentPrice = [...data.recommended, ...data.rest].find((row) => row.current)?.price;
|
|
421
608
|
const map = (group) => (row) => ({
|
|
422
609
|
group,
|
|
423
610
|
tag: row.tag,
|
|
424
611
|
providerName: row.providerName,
|
|
425
612
|
current: row.current,
|
|
426
613
|
score: row.score,
|
|
427
|
-
detail: `${row.quantization} · ${row.tps} t/s ·
|
|
428
|
-
|
|
614
|
+
detail: `${row.quantization} · ${row.tps} t/s · ${row.contextLength.toLocaleString()} ctx · ${row.uptime === null ? "N/A" : `${row.uptime.toFixed(2)}%`} ${i18n.t("panel.uptime")}`,
|
|
615
|
+
priceLabel: providerPriceLabel(row, i18n),
|
|
616
|
+
savingsLabel: currentPrice && !row.current ? (() => {
|
|
617
|
+
const value = estimatedSavings(currentPrice, row.price);
|
|
618
|
+
return value === null ? null : i18n.t("panel.savings", { percent: value });
|
|
619
|
+
})() : null,
|
|
620
|
+
scoreLabel: row.current ? `${i18n.t("panel.current")} · ${row.score.toFixed(1)}` : row.score.toFixed(1),
|
|
621
|
+
...providerInsight(row, i18n)
|
|
429
622
|
});
|
|
430
623
|
return [...data.recommended.map(map(i18n.t("panel.group.recommended"))), ...data.rest.map(map(i18n.t("panel.group.rest")))];
|
|
431
624
|
}
|
|
@@ -517,12 +710,17 @@ const panelStyle = {
|
|
|
517
710
|
overflow: "hidden",
|
|
518
711
|
display: "flex",
|
|
519
712
|
flexDirection: "column",
|
|
520
|
-
border:
|
|
713
|
+
border: borderL2,
|
|
521
714
|
borderRadius: 16,
|
|
522
|
-
background:
|
|
523
|
-
color:
|
|
524
|
-
boxShadow: "0 24px 80px rgba(0,0,0,.5)"
|
|
715
|
+
background: bgLayer1,
|
|
716
|
+
color: textPrimary,
|
|
717
|
+
boxShadow: "0 24px 80px rgba(0,0,0,.5)",
|
|
718
|
+
outline: "none"
|
|
525
719
|
};
|
|
720
|
+
function freshnessLabel(updatedAt, now, i18n = createI18n("zh-CN")) {
|
|
721
|
+
const minutes = Math.max(0, Math.floor((now - updatedAt) / 6e4));
|
|
722
|
+
return minutes === 0 ? i18n.t("panel.updatedJustNow") : i18n.t("panel.updatedMinutesAgo", { minutes });
|
|
723
|
+
}
|
|
526
724
|
const providerGridColumns = "minmax(110px,1fr) minmax(0,2fr) minmax(54px,auto)";
|
|
527
725
|
function ProviderOverlay(props) {
|
|
528
726
|
const i18n = useUiI18n();
|
|
@@ -533,6 +731,8 @@ function ProviderOverlay(props) {
|
|
|
533
731
|
current,
|
|
534
732
|
currentAddress
|
|
535
733
|
});
|
|
734
|
+
const dialogRef = (0, react.useRef)(null);
|
|
735
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
536
736
|
(0, react.useEffect)(() => {
|
|
537
737
|
if (!state.open) return;
|
|
538
738
|
if (!target.sessionId) props.store.close();
|
|
@@ -543,7 +743,41 @@ function ProviderOverlay(props) {
|
|
|
543
743
|
target.sessionId,
|
|
544
744
|
props.store
|
|
545
745
|
]);
|
|
746
|
+
(0, react.useEffect)(() => {
|
|
747
|
+
if (!state.open) return;
|
|
748
|
+
setNow(Date.now());
|
|
749
|
+
const timer = setInterval(() => setNow(Date.now()), 3e4);
|
|
750
|
+
return () => clearInterval(timer);
|
|
751
|
+
}, [state.open, state.updatedAt]);
|
|
752
|
+
(0, react.useEffect)(() => {
|
|
753
|
+
if (!state.open) return;
|
|
754
|
+
const previous = document.activeElement;
|
|
755
|
+
dialogRef.current?.focus();
|
|
756
|
+
return () => {
|
|
757
|
+
if (previous instanceof HTMLElement) previous.focus();
|
|
758
|
+
};
|
|
759
|
+
}, [state.open]);
|
|
546
760
|
if (!state.open) return null;
|
|
761
|
+
const onKeyDown = (event) => {
|
|
762
|
+
if (event.key === "Escape") {
|
|
763
|
+
event.stopPropagation();
|
|
764
|
+
props.store.close();
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (event.key !== "Tab" || !dialogRef.current) return;
|
|
768
|
+
const focusable = Array.from(dialogRef.current.querySelectorAll("button:not(:disabled), select:not(:disabled), input:not(:disabled), summary, [tabindex]:not([tabindex=\"-1\"])"));
|
|
769
|
+
if (focusable.length === 0) return;
|
|
770
|
+
const first = focusable[0];
|
|
771
|
+
const last = focusable.at(-1);
|
|
772
|
+
const active = document.activeElement;
|
|
773
|
+
if (event.shiftKey && (active === first || active === dialogRef.current)) {
|
|
774
|
+
event.preventDefault();
|
|
775
|
+
last.focus();
|
|
776
|
+
} else if (!event.shiftKey && active === last) {
|
|
777
|
+
event.preventDefault();
|
|
778
|
+
first.focus();
|
|
779
|
+
}
|
|
780
|
+
};
|
|
547
781
|
const rows = state.data ? providerRows(state.data, i18n) : [];
|
|
548
782
|
const credentialReady = state.credential?.configured === true;
|
|
549
783
|
const columns = providerTableColumns(i18n);
|
|
@@ -552,39 +786,49 @@ function ProviderOverlay(props) {
|
|
|
552
786
|
return (0, react.createElement)("div", {
|
|
553
787
|
style: backdrop,
|
|
554
788
|
role: "presentation",
|
|
789
|
+
onKeyDown,
|
|
555
790
|
onMouseDown: (event) => {
|
|
556
791
|
if (event.target === event.currentTarget) props.store.close();
|
|
557
792
|
}
|
|
558
793
|
}, (0, react.createElement)("section", {
|
|
794
|
+
ref: dialogRef,
|
|
559
795
|
role: "dialog",
|
|
560
796
|
"aria-modal": true,
|
|
561
797
|
"aria-label": i18n.t("panel.title"),
|
|
798
|
+
tabIndex: -1,
|
|
562
799
|
style: panelStyle
|
|
563
800
|
}, (0, react.createElement)("header", { style: {
|
|
564
801
|
display: "flex",
|
|
565
802
|
alignItems: "start",
|
|
566
803
|
justifyContent: "space-between",
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
804
|
+
gap: 16,
|
|
805
|
+
padding: "18px 22px 14px",
|
|
806
|
+
borderBottom: borderL2
|
|
807
|
+
} }, (0, react.createElement)("div", { style: {
|
|
808
|
+
minWidth: 0,
|
|
809
|
+
flex: 1
|
|
810
|
+
} }, (0, react.createElement)("h2", { style: {
|
|
570
811
|
margin: 0,
|
|
571
812
|
fontSize: 20
|
|
572
|
-
} }, i18n.t("panel.title")), (0, react.createElement)("
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
813
|
+
} }, i18n.t("panel.title")), (0, react.createElement)("label", { style: {
|
|
814
|
+
display: "block",
|
|
815
|
+
margin: "10px 0 5px",
|
|
816
|
+
color: textTertiary,
|
|
817
|
+
fontSize: 10,
|
|
818
|
+
fontWeight: 600,
|
|
819
|
+
letterSpacing: ".06em",
|
|
820
|
+
textTransform: "uppercase"
|
|
821
|
+
} }, i18n.t("panel.currentModel")), (0, react.createElement)("select", {
|
|
577
822
|
"aria-label": i18n.t("panel.modelSelect"),
|
|
578
823
|
value: selectedValue,
|
|
579
824
|
disabled: !credentialReady || state.models.length === 0 || state.status === "applying",
|
|
580
825
|
style: {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
padding: "7px 10px",
|
|
826
|
+
width: "min(420px, 100%)",
|
|
827
|
+
padding: "8px 10px",
|
|
584
828
|
borderRadius: 8,
|
|
585
|
-
border:
|
|
586
|
-
background:
|
|
587
|
-
color:
|
|
829
|
+
border: borderL2,
|
|
830
|
+
background: bgLayer2,
|
|
831
|
+
color: textPrimary
|
|
588
832
|
},
|
|
589
833
|
onChange: (event) => {
|
|
590
834
|
if (!target.sessionId) return;
|
|
@@ -601,12 +845,12 @@ function ProviderOverlay(props) {
|
|
|
601
845
|
style: {
|
|
602
846
|
display: "grid",
|
|
603
847
|
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
|
|
604
|
-
gap:
|
|
605
|
-
width: "min(
|
|
606
|
-
marginTop:
|
|
607
|
-
padding:
|
|
608
|
-
borderRadius:
|
|
609
|
-
background:
|
|
848
|
+
gap: 3,
|
|
849
|
+
width: "min(520px, 100%)",
|
|
850
|
+
marginTop: 10,
|
|
851
|
+
padding: 3,
|
|
852
|
+
borderRadius: 9,
|
|
853
|
+
background: bgLayer3
|
|
610
854
|
}
|
|
611
855
|
}, ...STRATEGY_OPTIONS.map((option) => {
|
|
612
856
|
const active = state.strategy === option.id;
|
|
@@ -618,14 +862,17 @@ function ProviderOverlay(props) {
|
|
|
618
862
|
onClick: () => {
|
|
619
863
|
if (target.sessionId && !active) consume(props.store.setStrategy(target.sessionId, option.id));
|
|
620
864
|
},
|
|
865
|
+
title: i18n.t(option.labelKey),
|
|
621
866
|
style: {
|
|
867
|
+
minHeight: 38,
|
|
622
868
|
border: active ? "1px solid #3b82f6" : "1px solid transparent",
|
|
623
|
-
borderRadius:
|
|
624
|
-
padding: "
|
|
625
|
-
background: active ? "rgba(37,99,235,.
|
|
626
|
-
color: active ? "#93c5fd" :
|
|
869
|
+
borderRadius: 6,
|
|
870
|
+
padding: "5px 8px",
|
|
871
|
+
background: active ? "rgba(37,99,235,.15)" : "transparent",
|
|
872
|
+
color: active ? "#93c5fd" : textSecondary,
|
|
627
873
|
cursor: active ? "default" : "pointer",
|
|
628
|
-
whiteSpace: "nowrap"
|
|
874
|
+
whiteSpace: "nowrap",
|
|
875
|
+
fontSize: 12
|
|
629
876
|
}
|
|
630
877
|
}, i18n.t(option.labelKey));
|
|
631
878
|
}))), (0, react.createElement)("div", { style: {
|
|
@@ -633,14 +880,28 @@ function ProviderOverlay(props) {
|
|
|
633
880
|
gap: 8
|
|
634
881
|
} }, (0, react.createElement)("button", {
|
|
635
882
|
type: "button",
|
|
883
|
+
title: i18n.t("panel.refresh"),
|
|
884
|
+
"aria-busy": state.status === "loading",
|
|
636
885
|
disabled: !credentialReady || !target.sessionId || state.status === "loading" || state.status === "applying",
|
|
637
886
|
onClick: () => {
|
|
638
887
|
if (target.sessionId) consume(props.store.refresh(target.sessionId));
|
|
888
|
+
},
|
|
889
|
+
style: {
|
|
890
|
+
...actionButtonStyle,
|
|
891
|
+
opacity: state.status === "loading" ? .6 : 1
|
|
892
|
+
}
|
|
893
|
+
}, (0, react.createElement)("span", {
|
|
894
|
+
"aria-hidden": true,
|
|
895
|
+
style: {
|
|
896
|
+
display: "inline-block",
|
|
897
|
+
marginRight: 6
|
|
639
898
|
}
|
|
640
|
-
}, i18n.t("panel.refresh")), (0, react.createElement)("button", {
|
|
899
|
+
}, "↻"), state.status === "loading" ? i18n.t("panel.refreshing") : i18n.t("panel.refresh")), (0, react.createElement)("button", {
|
|
641
900
|
type: "button",
|
|
901
|
+
title: i18n.t("panel.close"),
|
|
642
902
|
"aria-label": i18n.t("panel.close"),
|
|
643
|
-
onClick: () => props.store.close()
|
|
903
|
+
onClick: () => props.store.close(),
|
|
904
|
+
style: iconButtonStyle
|
|
644
905
|
}, "×"))), state.credential !== null && !state.credential.configured ? (0, react.createElement)("p", {
|
|
645
906
|
role: "status",
|
|
646
907
|
style: {
|
|
@@ -662,20 +923,50 @@ function ProviderOverlay(props) {
|
|
|
662
923
|
color: "#fca5a5",
|
|
663
924
|
background: "rgba(127,29,29,.35)"
|
|
664
925
|
}
|
|
665
|
-
}, state.error) : null, state.
|
|
926
|
+
}, state.error) : null, state.successTag && state.previousTag ? (0, react.createElement)("div", {
|
|
927
|
+
role: "status",
|
|
928
|
+
style: {
|
|
929
|
+
display: "flex",
|
|
930
|
+
alignItems: "center",
|
|
931
|
+
justifyContent: "space-between",
|
|
932
|
+
gap: 12,
|
|
933
|
+
margin: "12px 22px 0",
|
|
934
|
+
padding: "9px 11px",
|
|
935
|
+
borderRadius: 8,
|
|
936
|
+
color: "#bbf7d0",
|
|
937
|
+
background: "rgba(20,83,45,.24)",
|
|
938
|
+
border: "1px solid rgba(34,197,94,.22)",
|
|
939
|
+
fontSize: 12
|
|
940
|
+
}
|
|
941
|
+
}, (0, react.createElement)("span", null, i18n.t("panel.switchSuccess")), (0, react.createElement)("button", {
|
|
942
|
+
type: "button",
|
|
943
|
+
disabled: state.status === "applying",
|
|
944
|
+
onClick: () => {
|
|
945
|
+
if (target.sessionId && state.previousTag) consume(props.store.apply(target.sessionId, state.previousTag));
|
|
946
|
+
},
|
|
947
|
+
style: {
|
|
948
|
+
...actionButtonStyle,
|
|
949
|
+
height: 30,
|
|
950
|
+
color: "#bbf7d0",
|
|
951
|
+
whiteSpace: "nowrap"
|
|
952
|
+
}
|
|
953
|
+
}, i18n.t("panel.undo"))) : null, state.status === "loading" && rows.length === 0 ? (0, react.createElement)("p", { style: {
|
|
666
954
|
padding: 22,
|
|
667
|
-
color:
|
|
955
|
+
color: textTertiary
|
|
668
956
|
} }, i18n.t("panel.loading")) : null, credentialReady && rows.length > 0 ? (0, react.createElement)("div", { style: {
|
|
669
957
|
display: "grid",
|
|
670
958
|
gap: 2,
|
|
671
959
|
margin: "12px 22px 0",
|
|
672
|
-
color:
|
|
960
|
+
color: textTertiary,
|
|
673
961
|
fontSize: 11,
|
|
674
962
|
lineHeight: 1.5
|
|
675
963
|
} }, (0, react.createElement)("span", null, i18n.t("panel.switchHint")), (0, react.createElement)("span", { style: {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
} }, i18n.t("panel.uptimeDelayHint"))
|
|
964
|
+
fontSize: 10,
|
|
965
|
+
opacity: .8
|
|
966
|
+
} }, i18n.t("panel.uptimeDelayHint")), state.updatedAt === null ? null : (0, react.createElement)("span", { style: {
|
|
967
|
+
fontSize: 10,
|
|
968
|
+
opacity: .8
|
|
969
|
+
} }, freshnessLabel(state.updatedAt, now, i18n))) : null, (0, react.createElement)("div", { style: {
|
|
679
970
|
overflowY: "auto",
|
|
680
971
|
padding: "12px 14px 20px"
|
|
681
972
|
} }, ...rows.flatMap((row) => {
|
|
@@ -686,7 +977,7 @@ function ProviderOverlay(props) {
|
|
|
686
977
|
key: `group-${row.group}`,
|
|
687
978
|
style: {
|
|
688
979
|
margin: "14px 8px 8px",
|
|
689
|
-
color:
|
|
980
|
+
color: textTertiary,
|
|
690
981
|
fontSize: 12,
|
|
691
982
|
textTransform: "uppercase",
|
|
692
983
|
letterSpacing: ".06em"
|
|
@@ -699,13 +990,15 @@ function ProviderOverlay(props) {
|
|
|
699
990
|
gridTemplateColumns: providerGridColumns,
|
|
700
991
|
gap: 14,
|
|
701
992
|
padding: "0 14px 8px",
|
|
702
|
-
color:
|
|
993
|
+
color: textTertiary,
|
|
703
994
|
fontSize: 11,
|
|
704
995
|
letterSpacing: ".04em",
|
|
705
996
|
textTransform: "uppercase"
|
|
706
997
|
}
|
|
707
998
|
}, (0, react.createElement)("span", null, columns[0]), (0, react.createElement)("span", null, columns[1]), (0, react.createElement)("span", { style: { textAlign: "right" } }, columns[2])));
|
|
708
999
|
}
|
|
1000
|
+
const switching = state.applyingTag === row.tag;
|
|
1001
|
+
const switched = state.successTag === row.tag;
|
|
709
1002
|
nodes.push((0, react.createElement)("button", {
|
|
710
1003
|
key: row.tag,
|
|
711
1004
|
type: "button",
|
|
@@ -713,6 +1006,7 @@ function ProviderOverlay(props) {
|
|
|
713
1006
|
onClick: () => {
|
|
714
1007
|
if (target.sessionId) consume(props.store.apply(target.sessionId, row.tag));
|
|
715
1008
|
},
|
|
1009
|
+
title: row.current ? i18n.t("panel.current") : i18n.t("panel.switchAction"),
|
|
716
1010
|
style: {
|
|
717
1011
|
width: "100%",
|
|
718
1012
|
display: "grid",
|
|
@@ -722,28 +1016,124 @@ function ProviderOverlay(props) {
|
|
|
722
1016
|
padding: "12px 14px",
|
|
723
1017
|
marginBottom: 6,
|
|
724
1018
|
borderRadius: 10,
|
|
725
|
-
border: row.current ? "1px solid #3b82f6" :
|
|
726
|
-
background: row.current ? "rgba(37,99,235,.14)" :
|
|
1019
|
+
border: row.current ? "1px solid #3b82f6" : borderL2,
|
|
1020
|
+
background: row.current ? "rgba(37,99,235,.14)" : bgLayer3,
|
|
727
1021
|
color: "inherit",
|
|
728
1022
|
textAlign: "left",
|
|
729
|
-
cursor: row.current ? "default" : "pointer"
|
|
1023
|
+
cursor: row.current ? "default" : "pointer",
|
|
1024
|
+
transition: "background .15s ease,border-color .15s ease,transform .15s ease",
|
|
1025
|
+
opacity: state.status === "applying" && !switching ? .55 : 1
|
|
730
1026
|
}
|
|
731
|
-
}, (0, react.createElement)("
|
|
1027
|
+
}, (0, react.createElement)("span", { style: {
|
|
1028
|
+
display: "flex",
|
|
1029
|
+
alignItems: "center",
|
|
1030
|
+
gap: 8,
|
|
1031
|
+
minWidth: 0
|
|
1032
|
+
} }, (0, react.createElement)("strong", { style: {
|
|
1033
|
+
overflow: "hidden",
|
|
1034
|
+
textOverflow: "ellipsis"
|
|
1035
|
+
} }, row.providerName), (0, react.createElement)("small", { style: {
|
|
1036
|
+
flex: "0 0 auto",
|
|
1037
|
+
padding: "2px 6px",
|
|
1038
|
+
borderRadius: 999,
|
|
1039
|
+
color: "#a5b4fc",
|
|
1040
|
+
background: "rgba(99,102,241,.13)",
|
|
1041
|
+
fontSize: 9,
|
|
1042
|
+
fontWeight: 600
|
|
1043
|
+
} }, row.badge)), (0, react.createElement)("span", { style: {
|
|
1044
|
+
display: "grid",
|
|
1045
|
+
gap: 3,
|
|
1046
|
+
minWidth: 0
|
|
1047
|
+
} }, (0, react.createElement)("span", {
|
|
732
1048
|
title: row.detail,
|
|
733
1049
|
style: {
|
|
734
|
-
color:
|
|
735
|
-
fontSize:
|
|
1050
|
+
color: textTertiary,
|
|
1051
|
+
fontSize: 12,
|
|
1052
|
+
overflow: "hidden",
|
|
1053
|
+
textOverflow: "ellipsis",
|
|
1054
|
+
whiteSpace: "nowrap"
|
|
1055
|
+
}
|
|
1056
|
+
}, row.detail), (0, react.createElement)("span", {
|
|
1057
|
+
title: row.priceLabel,
|
|
1058
|
+
style: {
|
|
1059
|
+
color: textSecondary,
|
|
1060
|
+
fontSize: 11,
|
|
736
1061
|
overflow: "hidden",
|
|
737
1062
|
textOverflow: "ellipsis",
|
|
738
1063
|
whiteSpace: "nowrap"
|
|
739
1064
|
}
|
|
740
|
-
}, row.
|
|
741
|
-
color:
|
|
742
|
-
fontSize:
|
|
743
|
-
|
|
744
|
-
} }, row.
|
|
1065
|
+
}, row.priceLabel), row.savingsLabel ? (0, react.createElement)("small", { style: {
|
|
1066
|
+
color: "#86efac",
|
|
1067
|
+
fontSize: 10,
|
|
1068
|
+
fontWeight: 600
|
|
1069
|
+
} }, row.savingsLabel) : null), (0, react.createElement)("span", {
|
|
1070
|
+
title: row.breakdown,
|
|
1071
|
+
"aria-label": `${row.scoreLabel}; ${row.breakdown}`,
|
|
1072
|
+
style: {
|
|
1073
|
+
color: row.current ? "#60a5fa" : textSecondary,
|
|
1074
|
+
fontSize: 12,
|
|
1075
|
+
textAlign: "right",
|
|
1076
|
+
textDecoration: "underline dotted rgba(161,161,170,.6)",
|
|
1077
|
+
textUnderlineOffset: 3
|
|
1078
|
+
}
|
|
1079
|
+
}, switching ? i18n.t("panel.switching") : switched ? i18n.t("panel.switched") : row.scoreLabel)));
|
|
745
1080
|
return nodes;
|
|
746
|
-
}))
|
|
1081
|
+
}), state.history.length === 0 ? null : (0, react.createElement)("details", { style: {
|
|
1082
|
+
margin: "10px 8px 0",
|
|
1083
|
+
padding: "10px 12px",
|
|
1084
|
+
borderRadius: 10,
|
|
1085
|
+
border: borderL2
|
|
1086
|
+
} }, (0, react.createElement)("summary", { style: {
|
|
1087
|
+
cursor: "pointer",
|
|
1088
|
+
color: textTertiary,
|
|
1089
|
+
fontSize: 12,
|
|
1090
|
+
textTransform: "uppercase",
|
|
1091
|
+
letterSpacing: ".06em"
|
|
1092
|
+
} }, i18n.t("panel.history.title")), ...state.history.map((item) => {
|
|
1093
|
+
const revertable = item.fromTag !== null && state.data?.openrouterModel === item.model && state.data.currentTag !== item.fromTag;
|
|
1094
|
+
return (0, react.createElement)("div", {
|
|
1095
|
+
key: `${item.at}-${item.toTag}`,
|
|
1096
|
+
title: item.model,
|
|
1097
|
+
style: {
|
|
1098
|
+
display: "flex",
|
|
1099
|
+
alignItems: "center",
|
|
1100
|
+
gap: 10,
|
|
1101
|
+
padding: "8px 0",
|
|
1102
|
+
borderTop: borderL2,
|
|
1103
|
+
fontSize: 12,
|
|
1104
|
+
marginTop: 8
|
|
1105
|
+
}
|
|
1106
|
+
}, (0, react.createElement)("span", { style: {
|
|
1107
|
+
color: textTertiary,
|
|
1108
|
+
flex: "0 0 auto"
|
|
1109
|
+
} }, new Date(item.at).toLocaleTimeString(i18n.locale, {
|
|
1110
|
+
hour: "2-digit",
|
|
1111
|
+
minute: "2-digit"
|
|
1112
|
+
})), (0, react.createElement)("span", { style: {
|
|
1113
|
+
color: textSecondary,
|
|
1114
|
+
minWidth: 0,
|
|
1115
|
+
overflow: "hidden",
|
|
1116
|
+
textOverflow: "ellipsis",
|
|
1117
|
+
whiteSpace: "nowrap"
|
|
1118
|
+
} }, `${item.fromName ?? item.fromTag ?? "—"} → ${item.toName}`), item.fromTps !== null && item.toTps !== null ? (0, react.createElement)("span", { style: {
|
|
1119
|
+
color: textTertiary,
|
|
1120
|
+
flex: "0 0 auto"
|
|
1121
|
+
} }, `${item.fromTps} → ${item.toTps} t/s`) : null, revertable ? (0, react.createElement)("button", {
|
|
1122
|
+
type: "button",
|
|
1123
|
+
disabled: state.status === "applying",
|
|
1124
|
+
onClick: () => {
|
|
1125
|
+
if (target.sessionId && item.fromTag) consume(props.store.apply(target.sessionId, item.fromTag));
|
|
1126
|
+
},
|
|
1127
|
+
style: {
|
|
1128
|
+
...actionButtonStyle,
|
|
1129
|
+
height: 26,
|
|
1130
|
+
padding: "0 10px",
|
|
1131
|
+
marginLeft: "auto",
|
|
1132
|
+
fontSize: 12,
|
|
1133
|
+
whiteSpace: "nowrap"
|
|
1134
|
+
}
|
|
1135
|
+
}, i18n.t("panel.history.revert")) : null);
|
|
1136
|
+
})))));
|
|
747
1137
|
}
|
|
748
1138
|
|
|
749
1139
|
//#endregion
|
package/lib/index.js
CHANGED
|
@@ -104,8 +104,8 @@ async function applyProvider(deps, input) {
|
|
|
104
104
|
reasoningEfforts: effort.declaration,
|
|
105
105
|
openrouterModel: input.openrouterModel
|
|
106
106
|
};
|
|
107
|
-
const
|
|
108
|
-
const
|
|
107
|
+
const next = models.filter((model) => model.id === modelId || !(model.id.startsWith("@preset/") && model.openrouterModel === input.openrouterModel));
|
|
108
|
+
const index = next.findIndex((model) => model.id === modelId);
|
|
109
109
|
if (index >= 0) next[index] = {
|
|
110
110
|
...next[index],
|
|
111
111
|
...entry
|
|
@@ -275,8 +275,10 @@ function round(value, digits = 2) {
|
|
|
275
275
|
return Math.round((value + Number.EPSILON) * factor) / factor;
|
|
276
276
|
}
|
|
277
277
|
function uptimeMultiplier(uptime, config) {
|
|
278
|
-
|
|
279
|
-
const
|
|
278
|
+
if (uptime === null) return .85;
|
|
279
|
+
const interpolate = (value, low, high, lowScore, highScore) => lowScore + (highScore - lowScore) * (value - low) / (high - low);
|
|
280
|
+
const safety = uptime >= 99.5 ? 1 : uptime >= 99 ? interpolate(uptime, 99, 99.5, .97, 1) : uptime >= 98 ? interpolate(uptime, 98, 99, .9, .97) : uptime >= 95 ? interpolate(uptime, 95, 98, .75, .9) : uptime >= 90 ? interpolate(uptime, 90, 95, .5, .75) : interpolate(Math.max(0, uptime), 0, 90, .1, .5);
|
|
281
|
+
const configured = uptime >= config.uptimePenaltyThreshold ? 1 : interpolate(Math.max(config.uptimePenaltyThreshold - 5, uptime), config.uptimePenaltyThreshold - 5, config.uptimePenaltyThreshold, config.uptimePenaltyFactor, 1);
|
|
280
282
|
return Math.min(safety, configured);
|
|
281
283
|
}
|
|
282
284
|
function rankEndpoints(endpoints, config, currentTag) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type SupportedLocale = 'zh-CN' | 'en-US';
|
|
2
|
-
export type TranslationKey = 'panel.title' | 'panel.currentModelLoading' | 'panel.modelSelect' | 'panel.strategyGroup' | 'panel.refresh' | 'panel.close' | 'panel.loading' | 'panel.titleTrigger' | 'panel.unavailable' | 'panel.group.recommended' | 'panel.group.rest' | 'panel.column.provider' | 'panel.column.details' | 'panel.column.score' | 'panel.current' | 'panel.credentialMissing' | 'panel.switchHint' | 'panel.uptime' | 'panel.uptimeDelayHint' | 'panel.strategy.balanced' | 'panel.strategy.price' | 'panel.strategy.speed' | 'panel.strategy.context' | 'settings.title' | 'settings.summary' | 'settings.action.weights' | 'settings.action.priceBlend' | 'settings.action.reset' | 'settings.action.lock' | 'settings.action.unlock' | 'settings.expand' | 'settings.collapse' | 'settings.row.minContextTokens' | 'settings.row.recommendedCount' | 'settings.row.cacheTtlMs' | 'settings.row.weights.quantization' | 'settings.row.weights.speed' | 'settings.row.weights.price' | 'settings.row.weights.context' | 'settings.row.priceBlend.input' | 'settings.row.priceBlend.output' | 'settings.row.priceBlend.cache' | 'settings.row.uptimePenaltyThreshold' | 'settings.row.uptimePenaltyFactor' | 'settings.group.weights.title' | 'settings.group.weights.scope' | 'settings.group.priceBlend.title' | 'settings.group.priceBlend.scope' | 'settings.group.advanced.title' | 'settings.group.strategies.title' | 'settings.group.strategies.scope' | 'settings.dimension.quantization' | 'settings.dimension.speed' | 'settings.dimension.price' | 'settings.dimension.context' | 'settings.dimension.input' | 'settings.dimension.output' | 'settings.dimension.cache' | 'settings.connection.title' | 'settings.connection.loading' | 'settings.connection.configured' | 'settings.connection.missing' | 'settings.connection.error' | 'settings.connection.guide';
|
|
2
|
+
export type TranslationKey = 'panel.title' | 'panel.currentModelLoading' | 'panel.modelSelect' | 'panel.strategyGroup' | 'panel.refresh' | 'panel.close' | 'panel.loading' | 'panel.titleTrigger' | 'panel.unavailable' | 'panel.group.recommended' | 'panel.group.rest' | 'panel.column.provider' | 'panel.column.details' | 'panel.column.score' | 'panel.current' | 'panel.credentialMissing' | 'panel.switchHint' | 'panel.uptime' | 'panel.uptimeDelayHint' | 'panel.refreshing' | 'panel.switching' | 'panel.switched' | 'panel.switchAction' | 'panel.insight.quantization' | 'panel.insight.speed' | 'panel.insight.price' | 'panel.insight.context' | 'panel.currentModel' | 'panel.price.input' | 'panel.price.output' | 'panel.price.cache' | 'panel.savings' | 'panel.undo' | 'panel.switchSuccess' | 'panel.strategy.balanced' | 'panel.strategy.price' | 'panel.strategy.speed' | 'panel.strategy.context' | 'panel.updatedJustNow' | 'panel.updatedMinutesAgo' | 'panel.history.title' | 'panel.history.revert' | 'settings.title' | 'settings.summary' | 'settings.action.weights' | 'settings.action.priceBlend' | 'settings.action.reset' | 'settings.action.lock' | 'settings.action.unlock' | 'settings.expand' | 'settings.collapse' | 'settings.row.minContextTokens' | 'settings.row.recommendedCount' | 'settings.row.cacheTtlMs' | 'settings.row.weights.quantization' | 'settings.row.weights.speed' | 'settings.row.weights.price' | 'settings.row.weights.context' | 'settings.row.priceBlend.input' | 'settings.row.priceBlend.output' | 'settings.row.priceBlend.cache' | 'settings.row.uptimePenaltyThreshold' | 'settings.row.uptimePenaltyFactor' | 'settings.group.weights.title' | 'settings.group.weights.scope' | 'settings.group.priceBlend.title' | 'settings.group.priceBlend.scope' | 'settings.group.advanced.title' | 'settings.group.strategies.title' | 'settings.group.strategies.scope' | 'settings.dimension.quantization' | 'settings.dimension.speed' | 'settings.dimension.price' | 'settings.dimension.context' | 'settings.dimension.input' | 'settings.dimension.output' | 'settings.dimension.cache' | 'settings.connection.title' | 'settings.connection.loading' | 'settings.connection.configured' | 'settings.connection.missing' | 'settings.connection.error' | 'settings.connection.guide';
|
|
3
3
|
export declare const zh: Record<TranslationKey, string>;
|
|
4
4
|
export declare const en: Record<TranslationKey, string>;
|
|
5
5
|
export declare const LOCALE_NS = "openrouterProviders";
|
|
@@ -13,11 +13,26 @@ export interface ProviderPanelSnapshot {
|
|
|
13
13
|
} | null;
|
|
14
14
|
strategy: RankingStrategy;
|
|
15
15
|
error: string | null;
|
|
16
|
+
applyingTag: string | null;
|
|
17
|
+
successTag: string | null;
|
|
18
|
+
updatedAt: number | null;
|
|
19
|
+
previousTag: string | null;
|
|
16
20
|
credential: {
|
|
17
21
|
configured: boolean;
|
|
18
22
|
ref: string;
|
|
19
23
|
source: 'provider' | 'fallback';
|
|
20
24
|
} | null;
|
|
25
|
+
history: SwitchHistoryEntry[];
|
|
26
|
+
}
|
|
27
|
+
export interface SwitchHistoryEntry {
|
|
28
|
+
at: number;
|
|
29
|
+
model: string;
|
|
30
|
+
fromTag: string | null;
|
|
31
|
+
fromName: string | null;
|
|
32
|
+
fromTps: number | null;
|
|
33
|
+
toTag: string;
|
|
34
|
+
toName: string;
|
|
35
|
+
toTps: number | null;
|
|
21
36
|
}
|
|
22
37
|
export interface ConfiguredProviderModel {
|
|
23
38
|
provider: string;
|
|
@@ -38,5 +53,6 @@ export interface ProviderPanelStore {
|
|
|
38
53
|
apply(sessionId: string, tag: string): Promise<void>;
|
|
39
54
|
dispose(): void;
|
|
40
55
|
}
|
|
41
|
-
export
|
|
56
|
+
export type PanelStorage = Pick<Storage, 'getItem' | 'setItem'>;
|
|
57
|
+
export declare function createProviderPanelStore(fetchImpl?: typeof fetch, storage?: PanelStorage | undefined): ProviderPanelStore;
|
|
42
58
|
//# sourceMappingURL=provider-panel-store.d.ts.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactNode } from 'react';
|
|
2
2
|
import type { RecommendationResponse } from '../directory.js';
|
|
3
|
+
import type { RankedProvider } from '../types.js';
|
|
3
4
|
import type { RankingStrategy } from '../config.js';
|
|
4
5
|
import type { ProviderPanelStore } from './provider-panel-store.js';
|
|
5
6
|
interface SessionListLike {
|
|
@@ -17,7 +18,23 @@ export interface ProviderRowView {
|
|
|
17
18
|
detail: string;
|
|
18
19
|
score: number;
|
|
19
20
|
scoreLabel: string;
|
|
21
|
+
badge: string;
|
|
22
|
+
breakdown: string;
|
|
23
|
+
priceLabel: string;
|
|
24
|
+
savingsLabel: string | null;
|
|
20
25
|
}
|
|
26
|
+
export declare function providerInsight(row: RankedProvider, i18n?: {
|
|
27
|
+
locale: import("./i18n.js").SupportedLocale;
|
|
28
|
+
t(key: import("./i18n.js").TranslationKey, params?: Record<string, string | number>): string;
|
|
29
|
+
}): {
|
|
30
|
+
badge: string;
|
|
31
|
+
breakdown: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function providerPriceLabel(row: Pick<RankedProvider, 'price'>, i18n?: {
|
|
34
|
+
locale: import("./i18n.js").SupportedLocale;
|
|
35
|
+
t(key: import("./i18n.js").TranslationKey, params?: Record<string, string | number>): string;
|
|
36
|
+
}): string;
|
|
37
|
+
export declare function estimatedSavings(current: RankedProvider['price'], candidate: RankedProvider['price']): number | null;
|
|
21
38
|
export declare const STRATEGY_OPTIONS: ReadonlyArray<{
|
|
22
39
|
id: RankingStrategy;
|
|
23
40
|
labelKey: 'panel.strategy.balanced' | 'panel.strategy.price' | 'panel.strategy.speed' | 'panel.strategy.context';
|
|
@@ -39,6 +56,10 @@ export declare function ProviderSidebarTrigger(props: GlobalSlotProps & {
|
|
|
39
56
|
wide: boolean;
|
|
40
57
|
store: ProviderPanelStore;
|
|
41
58
|
}): ReactNode;
|
|
59
|
+
export declare function freshnessLabel(updatedAt: number, now: number, i18n?: {
|
|
60
|
+
locale: import("./i18n.js").SupportedLocale;
|
|
61
|
+
t(key: import("./i18n.js").TranslationKey, params?: Record<string, string | number>): string;
|
|
62
|
+
}): string;
|
|
42
63
|
export declare const providerGridColumns = "minmax(110px,1fr) minmax(0,2fr) minmax(54px,auto)";
|
|
43
64
|
export declare function ProviderOverlay(props: GlobalSlotProps & {
|
|
44
65
|
store: ProviderPanelStore;
|
package/package.json
CHANGED
|
@@ -1,23 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-openrouter-provider-advisor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "DSH web plugin that ranks OpenRouter upstream providers by quality, speed, cache-heavy price, context, and uptime, then safely switches the active route.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deepseek-harness",
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"openrouter",
|
|
10
|
+
"provider-routing",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/xuanfengtechx/dsh-openrouter-provider-advisor.git"
|
|
16
|
+
},
|
|
7
17
|
"homepage": "https://github.com/xuanfengtechx/dsh-openrouter-provider-advisor#readme",
|
|
8
|
-
"bugs": {
|
|
9
|
-
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/xuanfengtechx/dsh-openrouter-provider-advisor/issues"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
10
24
|
"type": "module",
|
|
11
25
|
"main": "lib/index.js",
|
|
12
26
|
"types": "lib/types/index.d.ts",
|
|
13
27
|
"exports": {
|
|
14
|
-
".": {
|
|
15
|
-
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./lib/types/index.d.ts",
|
|
30
|
+
"default": "./lib/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./client": {
|
|
33
|
+
"types": "./lib/types/client/index.d.ts",
|
|
34
|
+
"default": "./lib/client.js"
|
|
35
|
+
},
|
|
16
36
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
17
37
|
"./package.json": "./package.json"
|
|
18
38
|
},
|
|
19
39
|
"dsh": {
|
|
20
|
-
"bundle": {
|
|
40
|
+
"bundle": {
|
|
41
|
+
"patch": "./cordis.patch.yml"
|
|
42
|
+
},
|
|
21
43
|
"client": {
|
|
22
44
|
"platform": "web",
|
|
23
45
|
"inject": [
|
|
@@ -71,7 +93,19 @@
|
|
|
71
93
|
"typescript": "^5.9.2",
|
|
72
94
|
"vitest": "^4.1.8"
|
|
73
95
|
},
|
|
74
|
-
"engines": {
|
|
96
|
+
"engines": {
|
|
97
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
98
|
+
},
|
|
75
99
|
"license": "MIT",
|
|
76
|
-
"files": [
|
|
100
|
+
"files": [
|
|
101
|
+
"lib/**/*.js",
|
|
102
|
+
"lib/types/**/*.d.ts",
|
|
103
|
+
"cordis.patch.yml",
|
|
104
|
+
"dsh.plugin.json",
|
|
105
|
+
"README.md",
|
|
106
|
+
"README_EN.md",
|
|
107
|
+
"CHANGELOG.md",
|
|
108
|
+
"docs/images/*.png",
|
|
109
|
+
"LICENSE"
|
|
110
|
+
]
|
|
77
111
|
}
|