dsh-model-rotator 0.2.1 → 0.2.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/client.js +87 -15
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -74,7 +74,7 @@ window.__ModuleLoader__.load({
|
|
|
74
74
|
"models.channelOff": "全部关闭",
|
|
75
75
|
"models.expandAll": "全部展开",
|
|
76
76
|
"models.collapseAll": "全部收起",
|
|
77
|
-
"models.search": "
|
|
77
|
+
"models.search": "搜索模型…(空格分词;忽略 - _ . / 与大小写)",
|
|
78
78
|
"models.searchClear": "清除",
|
|
79
79
|
"models.searchHits": "命中 {n} / {total} 个模型",
|
|
80
80
|
"models.searchNone": "没有匹配的模型",
|
|
@@ -166,7 +166,7 @@ window.__ModuleLoader__.load({
|
|
|
166
166
|
"models.channelOff": "all off",
|
|
167
167
|
"models.expandAll": "Expand all",
|
|
168
168
|
"models.collapseAll": "Collapse all",
|
|
169
|
-
"models.search": "Search models… (
|
|
169
|
+
"models.search": "Search models… (tokens; - _ . / and case are ignored)",
|
|
170
170
|
"models.searchClear": "Clear",
|
|
171
171
|
"models.searchHits": "{n} of {total} models match",
|
|
172
172
|
"models.searchNone": "No model matches",
|
|
@@ -382,18 +382,74 @@ window.__ModuleLoader__.load({
|
|
|
382
382
|
* Declared with the state, above every reader: the header, the counter and
|
|
383
383
|
* the rows all call it during the same render pass.
|
|
384
384
|
*/
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
385
|
+
// ── search matching ───────────────────────────────────────────────────────
|
|
386
|
+
//
|
|
387
|
+
// Two ideas, both borrowed from how other DSH search UIs solved the same
|
|
388
|
+
// problem, and both measured against this machine's 950-model roster:
|
|
389
|
+
//
|
|
390
|
+
// 1. NORMALISE before comparing. "glm53" should find "GLM-5.3": dropping the
|
|
391
|
+
// separators (- _ . / and whitespace) from both sides costs nothing and
|
|
392
|
+
// took that query from 0 hits to 58. Normalised containment is a superset
|
|
393
|
+
// of raw containment, so nothing that matched before stops matching.
|
|
394
|
+
// 2. Rank matches by QUALITY rather than filtering to a flat list, so a loose
|
|
395
|
+
// fallback can be offered without making the list unusable.
|
|
396
|
+
//
|
|
397
|
+
// The fallback is subsequence (fzf-style: every query character in order, not
|
|
398
|
+
// necessarily adjacent), which is what makes an abbreviation like "ds" reach
|
|
399
|
+
// "DeepSeek". It is deliberately the weaker tier and guarded: without the
|
|
400
|
+
// length gate a single letter matched the entire roster, and even at two
|
|
401
|
+
// characters only 22% of its hits were actually DeepSeek-family models, so it
|
|
402
|
+
// is ranked below rather than mixed in with the substring hits.
|
|
403
|
+
const SEPARATORS = /[-_./\s]+/g;
|
|
404
|
+
const normSearch = (value) => String(value || "").toLowerCase().replace(SEPARATORS, "");
|
|
405
|
+
|
|
406
|
+
/** Every character of q appears in s, in order (not necessarily adjacent). */
|
|
407
|
+
const isSubsequence = (s, q) => {
|
|
408
|
+
let at = 0;
|
|
409
|
+
for (let i = 0; i < s.length && at < q.length; i++) if (s[i] === q[at]) at += 1;
|
|
410
|
+
return at === q.length;
|
|
396
411
|
};
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Match quality for one haystack/token pair.
|
|
415
|
+
*
|
|
416
|
+
* 2 = normalised substring, 1 = subsequence only, 0 = no match. Substring
|
|
417
|
+
* ranks above subsequence because it is the precise one: "flash" matching
|
|
418
|
+
* "flash" is a better answer than "ds" matching every model whose name happens
|
|
419
|
+
* to contain a d then an s.
|
|
420
|
+
*/
|
|
421
|
+
const tierOf = (hay, token) => {
|
|
422
|
+
if (hay.includes(token)) return 2;
|
|
423
|
+
// Below two characters the fallback is far too loose to be useful: "a" is a
|
|
424
|
+
// subsequence of almost every name, so it would rank the roster at random.
|
|
425
|
+
if (token.length >= 2 && isSubsequence(hay, token)) return 1;
|
|
426
|
+
return 0;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* The rank a row gets under the current query, or 0 to drop it.
|
|
431
|
+
*
|
|
432
|
+
* Every whitespace-separated token must match, and the row takes the WEAKEST
|
|
433
|
+
* token's tier: one imprecise token is enough to qualify the row, but it still
|
|
434
|
+
* sorts below a row where every token was precise.
|
|
435
|
+
*/
|
|
436
|
+
const rankOf = (row) => {
|
|
437
|
+
const tokens = normSearch(query).length === 0
|
|
438
|
+
? []
|
|
439
|
+
: query.trim().split(/\s+/).map(normSearch).filter((t) => t !== "");
|
|
440
|
+
if (tokens.length === 0) return query.trim() === "" ? 1 : 0;
|
|
441
|
+
const hay = normSearch(String(row.provider) + "/" + String(row.model));
|
|
442
|
+
let weakest = 2;
|
|
443
|
+
for (const token of tokens) {
|
|
444
|
+
const tier = tierOf(hay, token);
|
|
445
|
+
if (tier === 0) return 0;
|
|
446
|
+
if (tier < weakest) weakest = tier;
|
|
447
|
+
}
|
|
448
|
+
return weakest;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/** Kept as the boolean face of the same rule, for callers that only filter. */
|
|
452
|
+
const matches = (row) => rankOf(row) > 0;
|
|
397
453
|
// True whenever the box holds something. Declared with the state, before
|
|
398
454
|
// anything that reads it: the box renders before the payload is inspected,
|
|
399
455
|
// so a declaration further down would be a temporal-dead-zone read.
|
|
@@ -606,7 +662,23 @@ window.__ModuleLoader__.load({
|
|
|
606
662
|
* counters the folded line needs so a collapsed channel never hides whether
|
|
607
663
|
* the match is inside it.
|
|
608
664
|
*/
|
|
609
|
-
const visibleModels = (channel) =>
|
|
665
|
+
const visibleModels = (channel) => {
|
|
666
|
+
const ranked = [];
|
|
667
|
+
channel.models.forEach((row, index) => {
|
|
668
|
+
const rank = rankOf(row);
|
|
669
|
+
if (rank > 0) ranked.push({ row, rank, index });
|
|
670
|
+
});
|
|
671
|
+
// Only sort while a query is active: the unfiltered roster keeps the host's
|
|
672
|
+
// own order, which is a meaningful ranking (in-rotation first) that this
|
|
673
|
+
// page should not second-guess.
|
|
674
|
+
if (query.trim() !== "") {
|
|
675
|
+
ranked.sort((a, b) => (b.rank - a.rank) || (a.index - b.index));
|
|
676
|
+
}
|
|
677
|
+
return ranked.map((entry) => entry.row);
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
/** Whether the channel has any match at all - the filter the section needs. */
|
|
681
|
+
const hasMatch = (channel) => channel.models.some(matches);
|
|
610
682
|
|
|
611
683
|
|
|
612
684
|
/** One collapsible line: chevron, name, counts. */
|
|
@@ -694,7 +766,7 @@ window.__ModuleLoader__.load({
|
|
|
694
766
|
// and counts the matches rather than every row the channel owns - a
|
|
695
767
|
// folded line that reported the unfiltered tally would contradict the
|
|
696
768
|
// rows below it.
|
|
697
|
-
const matched = group.channels.filter(
|
|
769
|
+
const matched = group.channels.filter(hasMatch);
|
|
698
770
|
if (matched.length === 0) continue;
|
|
699
771
|
// Searching should reveal, not hide: a hit inside a folded channel would
|
|
700
772
|
// otherwise be invisible, which is exactly what the box was opened to fix.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model-rotator",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Model auto-ranking and transparent failover for DeepSeek Harness: ranks discovered models by measured connectivity and speed, switches to the next candidate inside the same turn when the current one is unusable, and ships a grouped model list with per-channel switches. Never modifies model or provider configuration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|