dsh-openrouter-provider-advisor 0.1.1 → 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 +24 -0
- package/lib/client.js +212 -39
- package/lib/index.js +2 -2
- package/lib/types/client/i18n.d.ts +1 -1
- package/lib/types/client/provider-panel-store.d.ts +13 -1
- package/lib/types/client/provider-panel.d.ts +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
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
|
+
|
|
5
28
|
## [0.1.1] - 2026-08-24
|
|
6
29
|
|
|
7
30
|
### Added
|
|
@@ -28,5 +51,6 @@ All notable changes to this project are documented here.
|
|
|
28
51
|
|
|
29
52
|
- Initial public release.
|
|
30
53
|
|
|
54
|
+
[0.1.2]: https://github.com/xuanfengtechx/dsh-openrouter-provider-advisor/compare/v0.1.1...v0.1.2
|
|
31
55
|
[0.1.1]: https://github.com/xuanfengtechx/dsh-openrouter-provider-advisor/compare/v0.1.0...v0.1.1
|
|
32
56
|
[0.1.0]: https://github.com/xuanfengtechx/dsh-openrouter-provider-advisor/releases/tag/v0.1.0
|
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,13 +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
78
|
credential: null,
|
|
46
79
|
applyingTag: null,
|
|
47
80
|
successTag: null,
|
|
48
81
|
updatedAt: null,
|
|
49
|
-
previousTag: null
|
|
82
|
+
previousTag: null,
|
|
83
|
+
history: readHistory(storage)
|
|
50
84
|
};
|
|
51
85
|
const listeners = /* @__PURE__ */ new Set();
|
|
52
86
|
let generation = 0;
|
|
@@ -124,7 +158,6 @@ function createProviderPanelStore(fetchImpl = fetch) {
|
|
|
124
158
|
open: true,
|
|
125
159
|
status: "loading",
|
|
126
160
|
sessionId,
|
|
127
|
-
strategy: "balanced",
|
|
128
161
|
data: null,
|
|
129
162
|
models: [],
|
|
130
163
|
selected: null,
|
|
@@ -157,7 +190,7 @@ function createProviderPanelStore(fetchImpl = fetch) {
|
|
|
157
190
|
});
|
|
158
191
|
return;
|
|
159
192
|
}
|
|
160
|
-
await load(sessionId, false, selected
|
|
193
|
+
await load(sessionId, false, selected);
|
|
161
194
|
} catch (error) {
|
|
162
195
|
if (operation.id !== generation || operation.signal.aborted) return;
|
|
163
196
|
publish({
|
|
@@ -175,7 +208,6 @@ function createProviderPanelStore(fetchImpl = fetch) {
|
|
|
175
208
|
publish({
|
|
176
209
|
open: false,
|
|
177
210
|
status: snapshot.data ? "ready" : "idle",
|
|
178
|
-
strategy: "balanced",
|
|
179
211
|
error: null
|
|
180
212
|
});
|
|
181
213
|
},
|
|
@@ -193,6 +225,9 @@ function createProviderPanelStore(fetchImpl = fetch) {
|
|
|
193
225
|
await load(sessionId, false, selected);
|
|
194
226
|
},
|
|
195
227
|
async setStrategy(sessionId, strategy) {
|
|
228
|
+
try {
|
|
229
|
+
storage?.setItem(STRATEGY_KEY, strategy);
|
|
230
|
+
} catch {}
|
|
196
231
|
publish({ strategy });
|
|
197
232
|
await load(sessionId, false, snapshot.selected, strategy);
|
|
198
233
|
},
|
|
@@ -224,15 +259,36 @@ function createProviderPanelStore(fetchImpl = fetch) {
|
|
|
224
259
|
...row,
|
|
225
260
|
current: row.tag === tag
|
|
226
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
|
+
}
|
|
227
287
|
publish({
|
|
228
288
|
open: true,
|
|
229
289
|
status: "ready",
|
|
230
|
-
data
|
|
231
|
-
|
|
232
|
-
currentTag: tag,
|
|
233
|
-
recommended: snapshot.data.recommended.map(markCurrent),
|
|
234
|
-
rest: snapshot.data.rest.map(markCurrent)
|
|
235
|
-
} : null,
|
|
290
|
+
data,
|
|
291
|
+
history,
|
|
236
292
|
applyingTag: null,
|
|
237
293
|
successTag: tag,
|
|
238
294
|
previousTag: previousTag === tag ? snapshot.previousTag : previousTag,
|
|
@@ -301,6 +357,10 @@ const messages = {
|
|
|
301
357
|
"panel.strategy.price": "价格优先",
|
|
302
358
|
"panel.strategy.speed": "速度优先",
|
|
303
359
|
"panel.strategy.context": "上下文优先",
|
|
360
|
+
"panel.updatedJustNow": "数据刚刚更新",
|
|
361
|
+
"panel.updatedMinutesAgo": "数据更新于 {minutes} 分钟前",
|
|
362
|
+
"panel.history.title": "切换历史",
|
|
363
|
+
"panel.history.revert": "切回",
|
|
304
364
|
"settings.title": "OpenRouter 供应商",
|
|
305
365
|
"settings.summary": "推荐数量、综合最佳权重与全局价格模型。",
|
|
306
366
|
"settings.action.weights": "应用综合权重",
|
|
@@ -382,6 +442,10 @@ const messages = {
|
|
|
382
442
|
"panel.strategy.price": "Price first",
|
|
383
443
|
"panel.strategy.speed": "Speed first",
|
|
384
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",
|
|
385
449
|
"settings.title": "OpenRouter Providers",
|
|
386
450
|
"settings.summary": "Recommended count, ranking weights, and price blend.",
|
|
387
451
|
"settings.action.weights": "Apply ranking weights",
|
|
@@ -460,13 +524,20 @@ function useUiI18n() {
|
|
|
460
524
|
|
|
461
525
|
//#endregion
|
|
462
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))";
|
|
463
534
|
const actionButtonStyle = {
|
|
464
535
|
height: 36,
|
|
465
536
|
padding: "0 12px",
|
|
466
537
|
borderRadius: 8,
|
|
467
|
-
border:
|
|
468
|
-
background:
|
|
469
|
-
color:
|
|
538
|
+
border: borderL2,
|
|
539
|
+
background: bgLayer3,
|
|
540
|
+
color: textPrimary,
|
|
470
541
|
cursor: "pointer"
|
|
471
542
|
};
|
|
472
543
|
const iconButtonStyle = {
|
|
@@ -639,12 +710,17 @@ const panelStyle = {
|
|
|
639
710
|
overflow: "hidden",
|
|
640
711
|
display: "flex",
|
|
641
712
|
flexDirection: "column",
|
|
642
|
-
border:
|
|
713
|
+
border: borderL2,
|
|
643
714
|
borderRadius: 16,
|
|
644
|
-
background:
|
|
645
|
-
color:
|
|
646
|
-
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"
|
|
647
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
|
+
}
|
|
648
724
|
const providerGridColumns = "minmax(110px,1fr) minmax(0,2fr) minmax(54px,auto)";
|
|
649
725
|
function ProviderOverlay(props) {
|
|
650
726
|
const i18n = useUiI18n();
|
|
@@ -655,6 +731,8 @@ function ProviderOverlay(props) {
|
|
|
655
731
|
current,
|
|
656
732
|
currentAddress
|
|
657
733
|
});
|
|
734
|
+
const dialogRef = (0, react.useRef)(null);
|
|
735
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
658
736
|
(0, react.useEffect)(() => {
|
|
659
737
|
if (!state.open) return;
|
|
660
738
|
if (!target.sessionId) props.store.close();
|
|
@@ -665,7 +743,41 @@ function ProviderOverlay(props) {
|
|
|
665
743
|
target.sessionId,
|
|
666
744
|
props.store
|
|
667
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]);
|
|
668
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
|
+
};
|
|
669
781
|
const rows = state.data ? providerRows(state.data, i18n) : [];
|
|
670
782
|
const credentialReady = state.credential?.configured === true;
|
|
671
783
|
const columns = providerTableColumns(i18n);
|
|
@@ -674,13 +786,16 @@ function ProviderOverlay(props) {
|
|
|
674
786
|
return (0, react.createElement)("div", {
|
|
675
787
|
style: backdrop,
|
|
676
788
|
role: "presentation",
|
|
789
|
+
onKeyDown,
|
|
677
790
|
onMouseDown: (event) => {
|
|
678
791
|
if (event.target === event.currentTarget) props.store.close();
|
|
679
792
|
}
|
|
680
793
|
}, (0, react.createElement)("section", {
|
|
794
|
+
ref: dialogRef,
|
|
681
795
|
role: "dialog",
|
|
682
796
|
"aria-modal": true,
|
|
683
797
|
"aria-label": i18n.t("panel.title"),
|
|
798
|
+
tabIndex: -1,
|
|
684
799
|
style: panelStyle
|
|
685
800
|
}, (0, react.createElement)("header", { style: {
|
|
686
801
|
display: "flex",
|
|
@@ -688,7 +803,7 @@ function ProviderOverlay(props) {
|
|
|
688
803
|
justifyContent: "space-between",
|
|
689
804
|
gap: 16,
|
|
690
805
|
padding: "18px 22px 14px",
|
|
691
|
-
borderBottom:
|
|
806
|
+
borderBottom: borderL2
|
|
692
807
|
} }, (0, react.createElement)("div", { style: {
|
|
693
808
|
minWidth: 0,
|
|
694
809
|
flex: 1
|
|
@@ -698,7 +813,7 @@ function ProviderOverlay(props) {
|
|
|
698
813
|
} }, i18n.t("panel.title")), (0, react.createElement)("label", { style: {
|
|
699
814
|
display: "block",
|
|
700
815
|
margin: "10px 0 5px",
|
|
701
|
-
color:
|
|
816
|
+
color: textTertiary,
|
|
702
817
|
fontSize: 10,
|
|
703
818
|
fontWeight: 600,
|
|
704
819
|
letterSpacing: ".06em",
|
|
@@ -711,9 +826,9 @@ function ProviderOverlay(props) {
|
|
|
711
826
|
width: "min(420px, 100%)",
|
|
712
827
|
padding: "8px 10px",
|
|
713
828
|
borderRadius: 8,
|
|
714
|
-
border:
|
|
715
|
-
background:
|
|
716
|
-
color:
|
|
829
|
+
border: borderL2,
|
|
830
|
+
background: bgLayer2,
|
|
831
|
+
color: textPrimary
|
|
717
832
|
},
|
|
718
833
|
onChange: (event) => {
|
|
719
834
|
if (!target.sessionId) return;
|
|
@@ -735,7 +850,7 @@ function ProviderOverlay(props) {
|
|
|
735
850
|
marginTop: 10,
|
|
736
851
|
padding: 3,
|
|
737
852
|
borderRadius: 9,
|
|
738
|
-
background:
|
|
853
|
+
background: bgLayer3
|
|
739
854
|
}
|
|
740
855
|
}, ...STRATEGY_OPTIONS.map((option) => {
|
|
741
856
|
const active = state.strategy === option.id;
|
|
@@ -754,7 +869,7 @@ function ProviderOverlay(props) {
|
|
|
754
869
|
borderRadius: 6,
|
|
755
870
|
padding: "5px 8px",
|
|
756
871
|
background: active ? "rgba(37,99,235,.15)" : "transparent",
|
|
757
|
-
color: active ? "#93c5fd" :
|
|
872
|
+
color: active ? "#93c5fd" : textSecondary,
|
|
758
873
|
cursor: active ? "default" : "pointer",
|
|
759
874
|
whiteSpace: "nowrap",
|
|
760
875
|
fontSize: 12
|
|
@@ -837,18 +952,21 @@ function ProviderOverlay(props) {
|
|
|
837
952
|
}
|
|
838
953
|
}, i18n.t("panel.undo"))) : null, state.status === "loading" && rows.length === 0 ? (0, react.createElement)("p", { style: {
|
|
839
954
|
padding: 22,
|
|
840
|
-
color:
|
|
955
|
+
color: textTertiary
|
|
841
956
|
} }, i18n.t("panel.loading")) : null, credentialReady && rows.length > 0 ? (0, react.createElement)("div", { style: {
|
|
842
957
|
display: "grid",
|
|
843
958
|
gap: 2,
|
|
844
959
|
margin: "12px 22px 0",
|
|
845
|
-
color:
|
|
960
|
+
color: textTertiary,
|
|
846
961
|
fontSize: 11,
|
|
847
962
|
lineHeight: 1.5
|
|
848
963
|
} }, (0, react.createElement)("span", null, i18n.t("panel.switchHint")), (0, react.createElement)("span", { style: {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
} }, 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: {
|
|
852
970
|
overflowY: "auto",
|
|
853
971
|
padding: "12px 14px 20px"
|
|
854
972
|
} }, ...rows.flatMap((row) => {
|
|
@@ -859,7 +977,7 @@ function ProviderOverlay(props) {
|
|
|
859
977
|
key: `group-${row.group}`,
|
|
860
978
|
style: {
|
|
861
979
|
margin: "14px 8px 8px",
|
|
862
|
-
color:
|
|
980
|
+
color: textTertiary,
|
|
863
981
|
fontSize: 12,
|
|
864
982
|
textTransform: "uppercase",
|
|
865
983
|
letterSpacing: ".06em"
|
|
@@ -872,7 +990,7 @@ function ProviderOverlay(props) {
|
|
|
872
990
|
gridTemplateColumns: providerGridColumns,
|
|
873
991
|
gap: 14,
|
|
874
992
|
padding: "0 14px 8px",
|
|
875
|
-
color:
|
|
993
|
+
color: textTertiary,
|
|
876
994
|
fontSize: 11,
|
|
877
995
|
letterSpacing: ".04em",
|
|
878
996
|
textTransform: "uppercase"
|
|
@@ -898,8 +1016,8 @@ function ProviderOverlay(props) {
|
|
|
898
1016
|
padding: "12px 14px",
|
|
899
1017
|
marginBottom: 6,
|
|
900
1018
|
borderRadius: 10,
|
|
901
|
-
border: row.current ? "1px solid #3b82f6" :
|
|
902
|
-
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,
|
|
903
1021
|
color: "inherit",
|
|
904
1022
|
textAlign: "left",
|
|
905
1023
|
cursor: row.current ? "default" : "pointer",
|
|
@@ -929,7 +1047,7 @@ function ProviderOverlay(props) {
|
|
|
929
1047
|
} }, (0, react.createElement)("span", {
|
|
930
1048
|
title: row.detail,
|
|
931
1049
|
style: {
|
|
932
|
-
color:
|
|
1050
|
+
color: textTertiary,
|
|
933
1051
|
fontSize: 12,
|
|
934
1052
|
overflow: "hidden",
|
|
935
1053
|
textOverflow: "ellipsis",
|
|
@@ -938,7 +1056,7 @@ function ProviderOverlay(props) {
|
|
|
938
1056
|
}, row.detail), (0, react.createElement)("span", {
|
|
939
1057
|
title: row.priceLabel,
|
|
940
1058
|
style: {
|
|
941
|
-
color:
|
|
1059
|
+
color: textSecondary,
|
|
942
1060
|
fontSize: 11,
|
|
943
1061
|
overflow: "hidden",
|
|
944
1062
|
textOverflow: "ellipsis",
|
|
@@ -952,7 +1070,7 @@ function ProviderOverlay(props) {
|
|
|
952
1070
|
title: row.breakdown,
|
|
953
1071
|
"aria-label": `${row.scoreLabel}; ${row.breakdown}`,
|
|
954
1072
|
style: {
|
|
955
|
-
color: row.current ? "#60a5fa" :
|
|
1073
|
+
color: row.current ? "#60a5fa" : textSecondary,
|
|
956
1074
|
fontSize: 12,
|
|
957
1075
|
textAlign: "right",
|
|
958
1076
|
textDecoration: "underline dotted rgba(161,161,170,.6)",
|
|
@@ -960,7 +1078,62 @@ function ProviderOverlay(props) {
|
|
|
960
1078
|
}
|
|
961
1079
|
}, switching ? i18n.t("panel.switching") : switched ? i18n.t("panel.switched") : row.scoreLabel)));
|
|
962
1080
|
return nodes;
|
|
963
|
-
}))
|
|
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
|
+
})))));
|
|
964
1137
|
}
|
|
965
1138
|
|
|
966
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
|
|
@@ -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.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' | '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";
|
|
@@ -22,6 +22,17 @@ export interface ProviderPanelSnapshot {
|
|
|
22
22
|
ref: string;
|
|
23
23
|
source: 'provider' | 'fallback';
|
|
24
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;
|
|
25
36
|
}
|
|
26
37
|
export interface ConfiguredProviderModel {
|
|
27
38
|
provider: string;
|
|
@@ -42,5 +53,6 @@ export interface ProviderPanelStore {
|
|
|
42
53
|
apply(sessionId: string, tag: string): Promise<void>;
|
|
43
54
|
dispose(): void;
|
|
44
55
|
}
|
|
45
|
-
export
|
|
56
|
+
export type PanelStorage = Pick<Storage, 'getItem' | 'setItem'>;
|
|
57
|
+
export declare function createProviderPanelStore(fetchImpl?: typeof fetch, storage?: PanelStorage | undefined): ProviderPanelStore;
|
|
46
58
|
//# sourceMappingURL=provider-panel-store.d.ts.map
|
|
@@ -56,6 +56,10 @@ export declare function ProviderSidebarTrigger(props: GlobalSlotProps & {
|
|
|
56
56
|
wide: boolean;
|
|
57
57
|
store: ProviderPanelStore;
|
|
58
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;
|
|
59
63
|
export declare const providerGridColumns = "minmax(110px,1fr) minmax(0,2fr) minmax(54px,auto)";
|
|
60
64
|
export declare function ProviderOverlay(props: GlobalSlotProps & {
|
|
61
65
|
store: ProviderPanelStore;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|