dsh-context 0.57.0 → 0.58.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/lib/client.js +255 -112
- package/lib/index.js +6 -3
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -354,6 +354,7 @@ window.__ModuleLoader__.load({
|
|
|
354
354
|
"ov.kpi.ofTotal": "共 {n} 个",
|
|
355
355
|
"ov.kpi.tokens": "累计消耗",
|
|
356
356
|
"ov.kpi.sessionsSub": "含 {n} 个会话",
|
|
357
|
+
"ov.kpi.pricedSub": "已计价 {n}/{total} 个会话",
|
|
357
358
|
"ov.kpi.toolSub": "工具执行 {dur}",
|
|
358
359
|
"ov.kpi.wallSub": "模型调用 {n}",
|
|
359
360
|
"ov.stats.empty": "统计范围内暂无 Token 用量",
|
|
@@ -734,6 +735,7 @@ window.__ModuleLoader__.load({
|
|
|
734
735
|
"ov.kpi.ofTotal": "of {n} total",
|
|
735
736
|
"ov.kpi.tokens": "Tokens Used",
|
|
736
737
|
"ov.kpi.sessionsSub": "across {n} sessions",
|
|
738
|
+
"ov.kpi.pricedSub": "{n} of {total} sessions priced",
|
|
737
739
|
"ov.kpi.toolSub": "tool runs {dur}",
|
|
738
740
|
"ov.kpi.wallSub": "{n} model calls",
|
|
739
741
|
"ov.stats.empty": "No billed tokens in this range",
|
|
@@ -2906,6 +2908,7 @@ window.__ModuleLoader__.load({
|
|
|
2906
2908
|
const BAR_W = 14;
|
|
2907
2909
|
const BAR_GAP = 2;
|
|
2908
2910
|
const STAGGER_CAP = 20;
|
|
2911
|
+
const STEP_FLAG_EVERY = 5;
|
|
2909
2912
|
const TURN_FILLS = ["color-mix(in srgb, var(--color-neutral-500) 12%, transparent)", "color-mix(in srgb, var(--color-neutral-500) 26%, transparent)"];
|
|
2910
2913
|
const LABEL_OVERHANG = 48;
|
|
2911
2914
|
const LABEL_GAP = 2;
|
|
@@ -2962,62 +2965,73 @@ window.__ModuleLoader__.load({
|
|
|
2962
2965
|
onMouseEnter: () => {
|
|
2963
2966
|
props.onHover(req.seq);
|
|
2964
2967
|
},
|
|
2965
|
-
children: [
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
2968
|
+
children: [
|
|
2969
|
+
props.flag !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2970
|
+
className: "lc-step-flag",
|
|
2971
|
+
"aria-hidden": "true",
|
|
2972
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2973
|
+
className: "lc-step-flag-label",
|
|
2974
|
+
children: props.flag
|
|
2975
|
+
})
|
|
2976
|
+
}) : null,
|
|
2977
|
+
marker !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2978
|
+
className: "lc-bar-marker",
|
|
2979
|
+
title: "✂ " + (markerAt !== null ? markerAt + " — " : "") + eventLabel(marker),
|
|
2980
|
+
children: "✂"
|
|
2981
|
+
}) : null,
|
|
2982
|
+
diverge ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2983
|
+
className: "lc-bar-up animate-lc-bar-in motion-reduce:animate-none",
|
|
2984
|
+
style: {
|
|
2985
|
+
bottom: `${props.downPx}px`,
|
|
2986
|
+
...enterStyle
|
|
2987
|
+
},
|
|
2988
|
+
children: CATS.map((c) => {
|
|
2989
|
+
const d = req[c.key] || 0;
|
|
2990
|
+
if (d <= 0) return null;
|
|
2991
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2992
|
+
"data-cat": c.key,
|
|
2993
|
+
className: "lc-cat-seg",
|
|
2994
|
+
style: {
|
|
2995
|
+
height: `${Math.max(1, Math.round(d * props.deltaScale))}px`,
|
|
2996
|
+
background: c.color
|
|
2997
|
+
}
|
|
2998
|
+
}, c.key);
|
|
2999
|
+
})
|
|
3000
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3001
|
+
className: "lc-bar-down animate-lc-bar-in motion-reduce:animate-none",
|
|
3002
|
+
style: {
|
|
3003
|
+
top: `${props.upPx}px`,
|
|
3004
|
+
...enterStyle
|
|
3005
|
+
},
|
|
3006
|
+
children: CATS.map((c) => {
|
|
3007
|
+
const d = req[c.key] || 0;
|
|
3008
|
+
if (d >= 0) return null;
|
|
3009
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3010
|
+
"data-cat": c.key,
|
|
3011
|
+
className: "lc-cat-seg",
|
|
3012
|
+
style: {
|
|
3013
|
+
height: `${Math.max(1, Math.round(-d * props.deltaScale))}px`,
|
|
3014
|
+
background: c.color
|
|
3015
|
+
}
|
|
3016
|
+
}, c.key);
|
|
3017
|
+
})
|
|
3018
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3019
|
+
className: "lc-bar-stack animate-lc-bar-in motion-reduce:animate-none",
|
|
3020
|
+
style: enterStyle,
|
|
3021
|
+
children: CATS.map((c) => {
|
|
3022
|
+
const v = req[c.key] || 0;
|
|
3023
|
+
if (!v) return null;
|
|
3024
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3025
|
+
"data-cat": c.key,
|
|
3026
|
+
className: "lc-cat-seg",
|
|
3027
|
+
style: {
|
|
3028
|
+
height: `${Math.max(1, Math.round(v / props.maxTotal * CHART_H))}px`,
|
|
3029
|
+
background: c.color
|
|
3030
|
+
}
|
|
3031
|
+
}, c.key);
|
|
3032
|
+
})
|
|
3019
3033
|
})
|
|
3020
|
-
|
|
3034
|
+
]
|
|
3021
3035
|
});
|
|
3022
3036
|
});
|
|
3023
3037
|
return function TrendChart(props) {
|
|
@@ -3371,6 +3385,7 @@ window.__ModuleLoader__.load({
|
|
|
3371
3385
|
selected: props.selectedSeq === req.seq,
|
|
3372
3386
|
hovered: props.hoveredSeq === req.seq,
|
|
3373
3387
|
inTurn: props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn,
|
|
3388
|
+
flag: props.granularity === "step" && (i + 1) % STEP_FLAG_EVERY === 0 ? i + 1 : null,
|
|
3374
3389
|
maxTotal,
|
|
3375
3390
|
upPx: delta ? upPx : void 0,
|
|
3376
3391
|
downPx: delta ? downPx : void 0,
|
|
@@ -5489,11 +5504,14 @@ window.__ModuleLoader__.load({
|
|
|
5489
5504
|
* The provider-id seam between dsh request envelopes and the models.dev
|
|
5490
5505
|
* registry: client/cost.ts resolves price-book branches through it, and
|
|
5491
5506
|
* host/fold.ts uses the DeepSeek resolution to split the session-cost totals
|
|
5492
|
-
* into peak/off-peak periods. Only the renames live here —
|
|
5493
|
-
*
|
|
5507
|
+
* into peak/off-peak periods. Only the renames live here — a dsh route key
|
|
5508
|
+
* that is not a registry id (user-declared gateways, `claude`/`codex`
|
|
5509
|
+
* subscriptions, org-prefixed catalogs) passes through and prices through
|
|
5510
|
+
* the model-side resolution index instead (client/cost.ts).
|
|
5494
5511
|
*/
|
|
5495
5512
|
const MODELS_DEV_PROVIDER_IDS = {
|
|
5496
5513
|
"deepseek-official": "deepseek",
|
|
5514
|
+
"deepseek-account": "deepseek",
|
|
5497
5515
|
"kimi-coding": "moonshotai",
|
|
5498
5516
|
"minimax-cn": "minimax",
|
|
5499
5517
|
"zai-coding-cn": "zhipuai"
|
|
@@ -5516,6 +5534,120 @@ window.__ModuleLoader__.load({
|
|
|
5516
5534
|
function toCurrency(usd, currency) {
|
|
5517
5535
|
return currency === "cny" ? usd / USD_PER_CNY : usd;
|
|
5518
5536
|
}
|
|
5537
|
+
/** Rate equality within the registry's float noise (sync drift prints 0.125000…003). */
|
|
5538
|
+
function sameRate(a, b) {
|
|
5539
|
+
const close = (x, y) => Math.abs(x - y) <= 1e-9 * Math.max(1, Math.abs(x));
|
|
5540
|
+
return close(a.hit, b.hit) && close(a.miss, b.miss) && close(a.write, b.write) && close(a.out, b.out);
|
|
5541
|
+
}
|
|
5542
|
+
/** One proven candidate rate off a built book branch, or null. */
|
|
5543
|
+
function rateOfEntry(value) {
|
|
5544
|
+
const v = value;
|
|
5545
|
+
if (v === null || typeof v !== "object") return null;
|
|
5546
|
+
const r = v;
|
|
5547
|
+
const num = (x) => typeof x === "number" && Number.isFinite(x);
|
|
5548
|
+
return num(r.hit) && num(r.miss) && num(r.write) && num(r.out) ? {
|
|
5549
|
+
hit: r.hit,
|
|
5550
|
+
miss: r.miss,
|
|
5551
|
+
write: r.write,
|
|
5552
|
+
out: r.out
|
|
5553
|
+
} : null;
|
|
5554
|
+
}
|
|
5555
|
+
/**
|
|
5556
|
+
* Build the resolution index over a proven book. `npmOf` carries each
|
|
5557
|
+
* provider's registry `npm` package (null when absent) — the vendor signal:
|
|
5558
|
+
* a provider whose package is `@ai-sdk/<its own id>` is the model's first
|
|
5559
|
+
* party, while mirrors and gateways ride `@ai-sdk/openai-compatible` or
|
|
5560
|
+
* someone else's package. Suffix tails are indexed alongside exact ids so a
|
|
5561
|
+
* short dsh spelling (`k3`) resolves book-wide too.
|
|
5562
|
+
*/
|
|
5563
|
+
function priceIndexOf(prices, npmOf) {
|
|
5564
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
5565
|
+
const push = (key, cand) => {
|
|
5566
|
+
const list = byModel.get(key);
|
|
5567
|
+
if (list === void 0) byModel.set(key, [cand]);
|
|
5568
|
+
else if (!list.some((c) => c.pid === cand.pid && sameRate(c.rate, cand.rate))) list.push(cand);
|
|
5569
|
+
};
|
|
5570
|
+
for (const pid of Object.keys(prices)) {
|
|
5571
|
+
const branch = branchOf(prices, pid);
|
|
5572
|
+
if (branch === null) continue;
|
|
5573
|
+
const npm = npmOf[pid];
|
|
5574
|
+
const primary = typeof npm === "string" && npm.startsWith("@ai-sdk/") && npm.slice(8) === pid;
|
|
5575
|
+
for (const mid of Object.keys(branch)) {
|
|
5576
|
+
const rate = rateOfEntry(branch[mid]);
|
|
5577
|
+
if (rate === null) continue;
|
|
5578
|
+
const lower = mid.toLowerCase();
|
|
5579
|
+
const cand = {
|
|
5580
|
+
pid,
|
|
5581
|
+
rate,
|
|
5582
|
+
primary
|
|
5583
|
+
};
|
|
5584
|
+
push(lower, cand);
|
|
5585
|
+
const parts = lower.split("-");
|
|
5586
|
+
for (let i = 1; i < parts.length; i++) push(parts.slice(i).join("-"), cand);
|
|
5587
|
+
}
|
|
5588
|
+
}
|
|
5589
|
+
return { byModel };
|
|
5590
|
+
}
|
|
5591
|
+
/**
|
|
5592
|
+
* The rate shared by a candidate group, or null: the largest equal-rate
|
|
5593
|
+
* group wins, a tie refuses. Agreeing mirrors are noise; disagreeing ones
|
|
5594
|
+
* price nothing rather than guess.
|
|
5595
|
+
*/
|
|
5596
|
+
function majorityRate(cands) {
|
|
5597
|
+
const groups = [];
|
|
5598
|
+
for (const c of cands) {
|
|
5599
|
+
const g = groups.find((g) => sameRate(g.rate, c.rate));
|
|
5600
|
+
if (g === void 0) groups.push({
|
|
5601
|
+
rate: c.rate,
|
|
5602
|
+
n: 1
|
|
5603
|
+
});
|
|
5604
|
+
else g.n++;
|
|
5605
|
+
}
|
|
5606
|
+
groups.sort((a, b) => b.n - a.n);
|
|
5607
|
+
return groups.length > 1 && groups[0].n === groups[1].n ? null : groups[0].rate;
|
|
5608
|
+
}
|
|
5609
|
+
/**
|
|
5610
|
+
* Pick one candidate group's rate: the model's own vendor (unique or
|
|
5611
|
+
* unanimous), then a provider the model id itself names (`deepseek-v4-flash`
|
|
5612
|
+
* under `deepseek`), then a lone carrier. Everything else prices null.
|
|
5613
|
+
*/
|
|
5614
|
+
function resolveCandidates(lower, cands, org) {
|
|
5615
|
+
if (org !== null) {
|
|
5616
|
+
const named = cands.filter((c) => c.pid === org || org.startsWith(c.pid + "-") || org.length >= 4 && org.startsWith(c.pid));
|
|
5617
|
+
if (named.length > 0) {
|
|
5618
|
+
const rate = majorityRate(named);
|
|
5619
|
+
if (rate !== null) return rate;
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5622
|
+
const primaries = cands.filter((c) => c.primary);
|
|
5623
|
+
if (primaries.length > 0) {
|
|
5624
|
+
const rate = majorityRate(primaries);
|
|
5625
|
+
if (rate !== null) return rate;
|
|
5626
|
+
}
|
|
5627
|
+
const prefixed = cands.filter((c) => lower.startsWith(c.pid + "-") || lower.startsWith(c.pid + "/"));
|
|
5628
|
+
if (prefixed.length > 0) {
|
|
5629
|
+
const rate = majorityRate(prefixed);
|
|
5630
|
+
if (rate !== null) return rate;
|
|
5631
|
+
}
|
|
5632
|
+
return cands.length === 1 ? cands[0].rate : null;
|
|
5633
|
+
}
|
|
5634
|
+
/**
|
|
5635
|
+
* Resolve a model id book-wide: the full id first, then its last `/`-segment
|
|
5636
|
+
* (an org-prefixed dialect whose exact spelling no registry provider lists).
|
|
5637
|
+
* Null when every tier refuses.
|
|
5638
|
+
*/
|
|
5639
|
+
function resolveRate(index, model) {
|
|
5640
|
+
const lower = model.toLowerCase();
|
|
5641
|
+
const slash = lower.lastIndexOf("/");
|
|
5642
|
+
const org = slash > 0 ? lower.slice(0, slash) : null;
|
|
5643
|
+
for (const key of slash > 0 ? [lower, lower.slice(slash + 1)] : [lower]) {
|
|
5644
|
+
const cands = index.byModel.get(key);
|
|
5645
|
+
if (cands === void 0) continue;
|
|
5646
|
+
const rate = resolveCandidates(key, cands, org);
|
|
5647
|
+
if (rate !== null) return rate;
|
|
5648
|
+
}
|
|
5649
|
+
return null;
|
|
5650
|
+
}
|
|
5519
5651
|
/** One rate triple at the doubled peak rate (the tooltip's `peak | off` pair). */
|
|
5520
5652
|
function peakOf(rate) {
|
|
5521
5653
|
return {
|
|
@@ -5555,22 +5687,16 @@ window.__ModuleLoader__.load({
|
|
|
5555
5687
|
* The book's rates for one folded (provider, model) bucket, or null when
|
|
5556
5688
|
* the book cannot price it: the dsh provider id resolves through
|
|
5557
5689
|
* modelsDevProviderOf (unmapped ids pass through) and prices by model id —
|
|
5558
|
-
* exact, case-insensitive, or suffix
|
|
5559
|
-
* falls back to
|
|
5560
|
-
*
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5690
|
+
* exact, case-insensitive, or suffix. A provider the book does not carry
|
|
5691
|
+
* falls back to the model-side resolution index, which names the vendor
|
|
5692
|
+
* from data (org segment, own-vendor SDK, id prefix, lone carrier) — never
|
|
5693
|
+
* from a per-model hardcode.
|
|
5694
|
+
*/
|
|
5695
|
+
function priceOf(book, provider, model) {
|
|
5696
|
+
if (book === null || book === void 0) return null;
|
|
5697
|
+
const direct = branchOf(book.prices, modelsDevProviderOf(provider));
|
|
5565
5698
|
if (direct !== null) return lookup(direct, model);
|
|
5566
|
-
|
|
5567
|
-
for (const models of Object.values(prices)) {
|
|
5568
|
-
const rate = lookup(models, model);
|
|
5569
|
-
if (rate === null) continue;
|
|
5570
|
-
if (found !== null) return null;
|
|
5571
|
-
found = rate;
|
|
5572
|
-
}
|
|
5573
|
-
return found;
|
|
5699
|
+
return resolveRate(book.index, model);
|
|
5574
5700
|
}
|
|
5575
5701
|
/**
|
|
5576
5702
|
* Price the session's cumulative billed-token totals. Cache reads bill at
|
|
@@ -5581,8 +5707,8 @@ window.__ModuleLoader__.load({
|
|
|
5581
5707
|
* Null when nothing was priced (no usage folded, no book yet, or no model
|
|
5582
5708
|
* the book prices), so the cell can show a dash.
|
|
5583
5709
|
*/
|
|
5584
|
-
function estimateSessionCost(usage,
|
|
5585
|
-
if (usage === null || usage === void 0 ||
|
|
5710
|
+
function estimateSessionCost(usage, book, currency) {
|
|
5711
|
+
if (usage === null || usage === void 0 || book === null || book === void 0) return null;
|
|
5586
5712
|
let total = 0;
|
|
5587
5713
|
let any = false;
|
|
5588
5714
|
for (const provider of Object.keys(usage)) {
|
|
@@ -5590,7 +5716,7 @@ window.__ModuleLoader__.load({
|
|
|
5590
5716
|
if (models === null) continue;
|
|
5591
5717
|
const deepseek = isDeepSeekProvider(provider);
|
|
5592
5718
|
for (const model of Object.keys(models)) {
|
|
5593
|
-
const rate = priceOf(
|
|
5719
|
+
const rate = priceOf(book, provider, model);
|
|
5594
5720
|
const periods = asRecord(models[model]);
|
|
5595
5721
|
if (rate === null || periods === null) continue;
|
|
5596
5722
|
for (const period of ["peak", "off"]) {
|
|
@@ -5662,7 +5788,7 @@ window.__ModuleLoader__.load({
|
|
|
5662
5788
|
return formatCost(amount, currency).replace(/0+$/, "").replace(/\.$/, "");
|
|
5663
5789
|
}
|
|
5664
5790
|
//#endregion
|
|
5665
|
-
//#region node_modules/.pnpm/@opencode-ai+models@0.0.
|
|
5791
|
+
//#region node_modules/.pnpm/@opencode-ai+models@0.0.87/node_modules/@opencode-ai/models/dist/error.js
|
|
5666
5792
|
/**
|
|
5667
5793
|
* The only error thrown by the models.dev client.
|
|
5668
5794
|
*
|
|
@@ -5679,7 +5805,7 @@ window.__ModuleLoader__.load({
|
|
|
5679
5805
|
}
|
|
5680
5806
|
};
|
|
5681
5807
|
//#endregion
|
|
5682
|
-
//#region node_modules/.pnpm/@opencode-ai+models@0.0.
|
|
5808
|
+
//#region node_modules/.pnpm/@opencode-ai+models@0.0.87/node_modules/@opencode-ai/models/dist/client.js
|
|
5683
5809
|
/**
|
|
5684
5810
|
* Creates a stateless models.dev client. Every method performs exactly one
|
|
5685
5811
|
* `GET` and nothing is ever cached — callers who want caching should wrap
|
|
@@ -5696,7 +5822,11 @@ window.__ModuleLoader__.load({
|
|
|
5696
5822
|
for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value);
|
|
5697
5823
|
let response;
|
|
5698
5824
|
try {
|
|
5699
|
-
|
|
5825
|
+
const url = new URL(path, base);
|
|
5826
|
+
const modelTypes = requestOptions?.modelTypes;
|
|
5827
|
+
if (modelTypes === "all") url.searchParams.set("type", "all");
|
|
5828
|
+
else if (modelTypes && modelTypes.length > 0) url.searchParams.set("type", modelTypes.join(","));
|
|
5829
|
+
response = await fetch(url, {
|
|
5700
5830
|
method: "GET",
|
|
5701
5831
|
headers,
|
|
5702
5832
|
signal: requestOptions?.signal
|
|
@@ -5741,10 +5871,12 @@ window.__ModuleLoader__.load({
|
|
|
5741
5871
|
* re-proves every field at the boundary — a non-conforming provider/model/
|
|
5742
5872
|
* cost entry drops whole. The book keeps EVERY provider that prices (keyed
|
|
5743
5873
|
* by the registry's own provider id), so a dsh provider id outside the
|
|
5744
|
-
* rename table still prices by direct passthrough
|
|
5745
|
-
*
|
|
5746
|
-
*
|
|
5747
|
-
*
|
|
5874
|
+
* rename table still prices by direct passthrough, and carries each
|
|
5875
|
+
* provider's `npm` package into the model-side resolution index
|
|
5876
|
+
* (cost.ts `priceIndexOf`) so unknown provider ids price by model id alone.
|
|
5877
|
+
* One fetch per page load, kicked on first subscribe; a failure degrades to
|
|
5878
|
+
* a visible state the cost cell notes, with a backed-off automatic retry —
|
|
5879
|
+
* never a spinner, and never an unhandled rejection.
|
|
5748
5880
|
*/
|
|
5749
5881
|
/** One finite non-negative registry figure, or null. */
|
|
5750
5882
|
function rateOf(value) {
|
|
@@ -5752,18 +5884,23 @@ window.__ModuleLoader__.load({
|
|
|
5752
5884
|
}
|
|
5753
5885
|
/**
|
|
5754
5886
|
* Extract the whole registry's per-model USD rates from a delivered
|
|
5755
|
-
* providers payload (`/api.json`), keyed by the registry's provider ids
|
|
5756
|
-
*
|
|
5757
|
-
*
|
|
5887
|
+
* providers payload (`/api.json`), keyed by the registry's provider ids,
|
|
5888
|
+
* with the resolution index built over it. Any shape failure skips just
|
|
5889
|
+
* that entry; a payload that is not a record at all returns null (the store
|
|
5890
|
+
* treats it as a failed fetch and retries).
|
|
5758
5891
|
*/
|
|
5759
5892
|
function pricesBookOf(value) {
|
|
5760
5893
|
const data = asRecord(value);
|
|
5761
5894
|
if (data === null || Array.isArray(data)) return null;
|
|
5762
5895
|
const book = {};
|
|
5896
|
+
const npmOf = {};
|
|
5763
5897
|
for (const providerId of Object.keys(data)) {
|
|
5764
5898
|
const provider = asRecord(data[providerId]);
|
|
5765
|
-
|
|
5899
|
+
if (provider === null) continue;
|
|
5900
|
+
const modelList = asRecord(provider.models);
|
|
5766
5901
|
if (modelList === null) continue;
|
|
5902
|
+
const npm = provider.npm;
|
|
5903
|
+
npmOf[providerId] = typeof npm === "string" ? npm : null;
|
|
5767
5904
|
const models = {};
|
|
5768
5905
|
for (const id of Object.keys(modelList)) {
|
|
5769
5906
|
const model = asRecord(modelList[id]);
|
|
@@ -5781,14 +5918,17 @@ window.__ModuleLoader__.load({
|
|
|
5781
5918
|
}
|
|
5782
5919
|
if (Object.keys(models).length > 0) book[providerId] = models;
|
|
5783
5920
|
}
|
|
5784
|
-
return
|
|
5921
|
+
return {
|
|
5922
|
+
prices: book,
|
|
5923
|
+
index: priceIndexOf(book, npmOf)
|
|
5924
|
+
};
|
|
5785
5925
|
}
|
|
5786
5926
|
const defaultLoader = () => make().providers();
|
|
5787
5927
|
/** The retry backoff base; each consecutive failure doubles the wait, capped at 3 doublings. */
|
|
5788
5928
|
const RETRY_BASE_MS = 3e4;
|
|
5789
5929
|
let loader = defaultLoader;
|
|
5790
5930
|
let snap = {
|
|
5791
|
-
|
|
5931
|
+
book: null,
|
|
5792
5932
|
failed: false
|
|
5793
5933
|
};
|
|
5794
5934
|
let inFlight = false;
|
|
@@ -5812,7 +5952,7 @@ window.__ModuleLoader__.load({
|
|
|
5812
5952
|
const book = pricesBookOf(await loader());
|
|
5813
5953
|
if (book !== null) {
|
|
5814
5954
|
snap = {
|
|
5815
|
-
|
|
5955
|
+
book,
|
|
5816
5956
|
failed: false
|
|
5817
5957
|
};
|
|
5818
5958
|
failures = 0;
|
|
@@ -5824,7 +5964,7 @@ window.__ModuleLoader__.load({
|
|
|
5824
5964
|
for (const fn of listeners) fn();
|
|
5825
5965
|
}
|
|
5826
5966
|
function kick() {
|
|
5827
|
-
if (snap.
|
|
5967
|
+
if (snap.book !== null || inFlight || timer !== null) return;
|
|
5828
5968
|
fire();
|
|
5829
5969
|
}
|
|
5830
5970
|
/** The useSyncExternalStore seam: subscribing also kicks the first fetch. */
|
|
@@ -6307,7 +6447,7 @@ window.__ModuleLoader__.load({
|
|
|
6307
6447
|
total
|
|
6308
6448
|
};
|
|
6309
6449
|
}
|
|
6310
|
-
function kpisOf(rows, listed,
|
|
6450
|
+
function kpisOf(rows, listed, book, currency) {
|
|
6311
6451
|
const usage = mergeCostUsage(...rows.map((row) => row.timeline?.cost));
|
|
6312
6452
|
const totals = usageTotalsOf(usage);
|
|
6313
6453
|
let turns = 0;
|
|
@@ -6319,7 +6459,7 @@ window.__ModuleLoader__.load({
|
|
|
6319
6459
|
let usageSessions = 0;
|
|
6320
6460
|
for (const row of rows) {
|
|
6321
6461
|
const cost = row.timeline?.cost;
|
|
6322
|
-
if (estimateSessionCost(cost,
|
|
6462
|
+
if (estimateSessionCost(cost, book, currency) !== null) costSessions++;
|
|
6323
6463
|
if (usageTotalsOf(cost) !== null) usageSessions++;
|
|
6324
6464
|
turns += turnsOf(row.timeline);
|
|
6325
6465
|
const timing = row.timeline?.timing;
|
|
@@ -6333,7 +6473,7 @@ window.__ModuleLoader__.load({
|
|
|
6333
6473
|
listed,
|
|
6334
6474
|
tokens: totals?.total ?? 0,
|
|
6335
6475
|
turns,
|
|
6336
|
-
cost: estimateSessionCost(usage,
|
|
6476
|
+
cost: estimateSessionCost(usage, book, currency),
|
|
6337
6477
|
costSessions,
|
|
6338
6478
|
cacheHit: totals === null ? null : cacheHitPercent(totals.cacheRead, totals.input + totals.cacheRead + totals.cacheWrite),
|
|
6339
6479
|
usageSessions,
|
|
@@ -7524,7 +7664,7 @@ window.__ModuleLoader__.load({
|
|
|
7524
7664
|
}
|
|
7525
7665
|
function OverviewBody(props) {
|
|
7526
7666
|
const open = (0, react.useSyncExternalStore)(overviewStore.subscribe, overviewStore.getSnapshot);
|
|
7527
|
-
const {
|
|
7667
|
+
const { book } = useModelPrices();
|
|
7528
7668
|
const snapshot = sessionsSnapshotOf(props);
|
|
7529
7669
|
const wsSnapshot = workspacesSnapshotOf(props);
|
|
7530
7670
|
const [range, setRange] = (0, react.useState)("30d");
|
|
@@ -7576,7 +7716,7 @@ window.__ModuleLoader__.load({
|
|
|
7576
7716
|
}] : counts;
|
|
7577
7717
|
const visible = sortRows(group === null ? scoped : scoped.filter((row) => inGroup(row, group, groups)), sort);
|
|
7578
7718
|
const paged = pageOf(visible, page);
|
|
7579
|
-
const kpi = kpisOf(ranged, allRows.length,
|
|
7719
|
+
const kpi = kpisOf(ranged, allRows.length, book, currency);
|
|
7580
7720
|
const days = aggregateDays(allRows);
|
|
7581
7721
|
const openOne = (id) => {
|
|
7582
7722
|
openSessionVia(ctx, id);
|
|
@@ -7681,7 +7821,10 @@ window.__ModuleLoader__.load({
|
|
|
7681
7821
|
}),
|
|
7682
7822
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7683
7823
|
className: "lc-stat-sub",
|
|
7684
|
-
children: t("ov.kpi.
|
|
7824
|
+
children: t("ov.kpi.pricedSub", {
|
|
7825
|
+
n: kpi.costSessions,
|
|
7826
|
+
total: kpi.sessions
|
|
7827
|
+
})
|
|
7685
7828
|
})
|
|
7686
7829
|
]
|
|
7687
7830
|
}),
|
|
@@ -7876,7 +8019,7 @@ window.__ModuleLoader__.load({
|
|
|
7876
8019
|
children: paged.items.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OverviewCard, {
|
|
7877
8020
|
row,
|
|
7878
8021
|
...groups?.[row.id] !== void 0 ? { group: groups[row.id] } : {},
|
|
7879
|
-
costLabel: cardCostOf(row,
|
|
8022
|
+
costLabel: cardCostOf(row, book, currency),
|
|
7880
8023
|
now,
|
|
7881
8024
|
onOpen: openOne
|
|
7882
8025
|
}, row.id))
|
|
@@ -7926,9 +8069,9 @@ window.__ModuleLoader__.load({
|
|
|
7926
8069
|
};
|
|
7927
8070
|
}
|
|
7928
8071
|
/** One card's priced cost label, or the dash (no book yet, nothing billed, unpriceable model). */
|
|
7929
|
-
function cardCostOf(row,
|
|
8072
|
+
function cardCostOf(row, book, currency) {
|
|
7930
8073
|
if (row.timeline?.cost === void 0) return "—";
|
|
7931
|
-
const cost = estimateSessionCost(row.timeline.cost,
|
|
8074
|
+
const cost = estimateSessionCost(row.timeline.cost, book, currency);
|
|
7932
8075
|
return cost === null ? "—" : formatCost(cost, currency);
|
|
7933
8076
|
}
|
|
7934
8077
|
//#endregion
|
|
@@ -10404,7 +10547,7 @@ window.__ModuleLoader__.load({
|
|
|
10404
10547
|
return function PluginInfo() {
|
|
10405
10548
|
const [latest, setLatest] = (0, react.useState)(null);
|
|
10406
10549
|
(0, react.useEffect)(() => {
|
|
10407
|
-
if ("0.
|
|
10550
|
+
if ("0.58.0".includes("-dev")) return;
|
|
10408
10551
|
let on = true;
|
|
10409
10552
|
fetchLatestVersion().then((v) => {
|
|
10410
10553
|
if (on && v) setLatest(v);
|
|
@@ -10413,8 +10556,8 @@ window.__ModuleLoader__.load({
|
|
|
10413
10556
|
on = false;
|
|
10414
10557
|
};
|
|
10415
10558
|
}, []);
|
|
10416
|
-
const update = latest !== null && isNewerVersion(latest, "0.
|
|
10417
|
-
const nameText = "dsh-context (v0.
|
|
10559
|
+
const update = latest !== null && isNewerVersion(latest, "0.58.0") ? latest : null;
|
|
10560
|
+
const nameText = "dsh-context (v0.58.0)";
|
|
10418
10561
|
const nameValue = [nameText];
|
|
10419
10562
|
if (update) nameValue.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10420
10563
|
className: "lc-pi-update",
|
|
@@ -10887,8 +11030,8 @@ window.__ModuleLoader__.load({
|
|
|
10887
11030
|
* with an off-peak bucket (DeepSeek's period-based list) shows the
|
|
10888
11031
|
* peak | off-peak pair.
|
|
10889
11032
|
*/
|
|
10890
|
-
function priceRowsOf(usage,
|
|
10891
|
-
if (usage === void 0 ||
|
|
11033
|
+
function priceRowsOf(usage, book) {
|
|
11034
|
+
if (usage === void 0 || book === null) return [];
|
|
10892
11035
|
const rows = [];
|
|
10893
11036
|
const multi = Object.keys(usage).length > 1;
|
|
10894
11037
|
for (const provider of Object.keys(usage)) {
|
|
@@ -10898,7 +11041,7 @@ window.__ModuleLoader__.load({
|
|
|
10898
11041
|
reaches here; the guard stays for the helper's own contract. */
|
|
10899
11042
|
if (models === null) continue;
|
|
10900
11043
|
for (const model of Object.keys(models)) {
|
|
10901
|
-
const rate = priceOf(
|
|
11044
|
+
const rate = priceOf(book, provider, model);
|
|
10902
11045
|
if (rate === null) continue;
|
|
10903
11046
|
const periods = asRecord(models[model]);
|
|
10904
11047
|
const deepseek = isDeepSeekProvider(provider);
|
|
@@ -10964,18 +11107,18 @@ window.__ModuleLoader__.load({
|
|
|
10964
11107
|
const { t, fmt } = kit;
|
|
10965
11108
|
return function StatsContext(props) {
|
|
10966
11109
|
const currency = props.locale === "zh" ? "cny" : "usd";
|
|
10967
|
-
const {
|
|
11110
|
+
const { book, failed } = useModelPrices();
|
|
10968
11111
|
const subUsage = useSubagentCost(props.sessionId);
|
|
10969
11112
|
const usage = mergeCostUsage(props.cost, subUsage) ?? void 0;
|
|
10970
|
-
const cost = estimateSessionCost(usage,
|
|
10971
|
-
const subCost = estimateSessionCost(subUsage,
|
|
11113
|
+
const cost = estimateSessionCost(usage, book, currency);
|
|
11114
|
+
const subCost = estimateSessionCost(subUsage, book, currency);
|
|
10972
11115
|
const fmtRate = (usd) => formatPriceRate(toCurrency(usd, currency), currency);
|
|
10973
|
-
const rows = priceRowsOf(usage,
|
|
11116
|
+
const rows = priceRowsOf(usage, book);
|
|
10974
11117
|
const deepseek = usage !== void 0 && Object.keys(usage).some((p) => isDeepSeekProvider(p));
|
|
10975
11118
|
const subDeepseek = subUsage !== null && Object.keys(subUsage).some((p) => isDeepSeekProvider(p));
|
|
10976
11119
|
const anyPair = rows.some((r) => r.offRate !== void 0);
|
|
10977
|
-
const unpriced = rows.length === 0 && usage !== void 0 && Object.keys(usage).length > 0 && (failed ||
|
|
10978
|
-
const subUnpriced = subUsage !== null && priceRowsOf(subUsage,
|
|
11120
|
+
const unpriced = rows.length === 0 && usage !== void 0 && Object.keys(usage).length > 0 && (failed || book !== null);
|
|
11121
|
+
const subUnpriced = subUsage !== null && priceRowsOf(subUsage, book).length === 0 && (failed || book !== null);
|
|
10979
11122
|
const costTip = [
|
|
10980
11123
|
t("stats.costTip") + (deepseek ? " " + t("stats.costTipDeepseek") : ""),
|
|
10981
11124
|
rows.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -12021,7 +12164,7 @@ window.__ModuleLoader__.load({
|
|
|
12021
12164
|
}
|
|
12022
12165
|
//#endregion
|
|
12023
12166
|
//#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/trendChart.css.mjs
|
|
12024
|
-
const css$9 = ".lc-chartrow{align-items:stretch;gap:6px;display:flex}.lc-axis{box-sizing:border-box;width:40px;height:150px;color:var(--dsw-alias-label-secondary);padding-top:18px;font-size:11px;position:relative}.lc-axis span{line-height:1;position:absolute;right:0}.lc-axis-top{top:13px}.lc-axis-q3{top:41px}.lc-axis-mid{top:69px}.lc-axis-q1{top:97px}.lc-axis-bot{top:125px}.lc-chart-wrap{flex:1;min-width:0;position:relative}.lc-chart-scroll{overscroll-behavior-x:contain;scrollbar-gutter:stable;padding-bottom:var(--dsh-scrollbar-width,8px);overflow:auto hidden}.lc-chart{box-sizing:border-box;align-items:flex-end;gap:2px;width:max-content;min-width:100%;height:130px;padding
|
|
12167
|
+
const css$9 = ".lc-chartrow{align-items:stretch;gap:6px;display:flex}.lc-axis{box-sizing:border-box;width:40px;height:150px;color:var(--dsw-alias-label-secondary);padding-top:18px;font-size:11px;position:relative}.lc-axis span{line-height:1;position:absolute;right:0}.lc-axis-top{top:13px}.lc-axis-q3{top:41px}.lc-axis-mid{top:69px}.lc-axis-q1{top:97px}.lc-axis-bot{top:125px}.lc-chart-wrap{flex:1;min-width:0;position:relative}.lc-chart-scroll{overscroll-behavior-x:contain;scrollbar-gutter:stable;padding-bottom:var(--dsh-scrollbar-width,8px);overflow:auto hidden}.lc-chart{box-sizing:border-box;align-items:flex-end;gap:2px;width:max-content;min-width:100%;height:130px;padding:18px 2px 0;display:flex;position:relative}.lc-grid{border-top:1px dashed var(--dsw-alias-border-l2);pointer-events:none;position:absolute;left:0;right:0}.lc-grid-top{top:18px}.lc-grid-q3{top:46px}.lc-grid-mid{top:74px}.lc-grid-q1{top:102px}.lc-grid-zero{border-top-style:solid;border-top-color:color-mix(in srgb, var(--color-neutral-500) 50%, transparent);top:130px}.lc-bar{cursor:pointer;outline-offset:1px;width:14px;height:100%;transition:opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), outline-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:2px;outline:1px dashed #0000;flex:none;align-items:flex-end;display:flex;position:relative}.lc-chart-dim .lc-bar{opacity:.35}.lc-chart-dim .lc-bar-in-turn{opacity:1}.lc-chart-dim .lc-turn{opacity:.35}.lc-chart-dim .lc-turn-on{opacity:1}.lc-chart-tip{z-index:5;box-sizing:border-box;overflow-wrap:break-word;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);max-width:100%;color:var(--dsw-alias-label-primary);pointer-events:none;border-radius:6px;flex-direction:column;gap:2px;padding:3px 8px;font-size:12px;display:flex;position:absolute;bottom:calc(100% + 4px);left:0;box-shadow:0 2px 8px #0000002e}.lc-chart-tip span+span{color:var(--dsw-alias-label-secondary)}.lc-bar-hovered{outline-color:var(--dsw-alias-brand-primary)}.lc-bar-selected{outline:2px solid var(--dsw-alias-label-primary);outline-offset:1px}.lc-bar-in-turn{background:color-mix(in srgb, var(--color-neutral-500) 14%, transparent)}.lc-bar-stack{flex-direction:column-reverse;width:100%;display:flex}.lc-bar-stack>div{width:100%}.lc-bar-stack,.lc-bar-up,.lc-bar-down{transform-origin:50% 100%}.lc-bar-down{transform-origin:50% 0}.lc-bar-stack>div:first-child{border-radius:0 0 2px 2px}.lc-bar-stack>div:last-child{border-radius:2px 2px 0 0}.lc-bar-stack>div:only-child{border-radius:2px}.lc-bar-up,.lc-bar-down{width:100%;display:flex;position:absolute;left:0}.lc-bar-up{flex-direction:column-reverse}.lc-bar-down{flex-direction:column}.lc-bar-up>div,.lc-bar-down>div{width:100%}.lc-bar-up>div:last-child{border-radius:2px 2px 0 0}.lc-bar-down>div:last-child{border-radius:0 0 2px 2px}.lc-cat-seg{transition:filter var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out)}.lc-chart[data-catdim] .lc-cat-seg{opacity:.35}.lc-chart[data-catdim=system] .lc-cat-seg[data-cat=system],.lc-chart[data-catdim=tools] .lc-cat-seg[data-cat=tools],.lc-chart[data-catdim=user] .lc-cat-seg[data-cat=user],.lc-chart[data-catdim=inject] .lc-cat-seg[data-cat=inject],.lc-chart[data-catdim=skill] .lc-cat-seg[data-cat=skill],.lc-chart[data-catdim=assistant] .lc-cat-seg[data-cat=assistant],.lc-chart[data-catdim=tool] .lc-cat-seg[data-cat=tool]{opacity:1;filter:brightness(1.18)}.lc-bar-marker{color:var(--dsw-alias-state-warn-primary);font-size:11px;position:absolute;top:-16px;left:50%;transform:translate(-50%)}.lc-step-flag{background:color-mix(in srgb, var(--color-neutral-500) 45%, transparent);pointer-events:none;width:1px;position:absolute;top:-17px;bottom:0;right:-1px}.lc-step-flag-label{box-sizing:border-box;border:1px solid color-mix(in srgb, var(--color-neutral-500) 45%, transparent);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:3px;padding:1px 3px;font-size:9px;font-weight:600;line-height:1;position:absolute;top:0;right:0}.lc-turns{box-sizing:border-box;gap:2px;width:max-content;min-width:100%;margin-top:4px;padding:0 2px;display:flex;overflow:hidden}.lc-turn{box-sizing:border-box;text-align:center;color:var(--dsw-alias-label-secondary);white-space:nowrap;cursor:pointer;height:14px;transition:filter var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:3px;flex:none;font-size:10px;font-weight:600;line-height:14px}.lc-turn-on{filter:brightness(1.35);color:var(--dsw-alias-label-primary)}.lc-turn-label{display:inline-block}";
|
|
12025
12168
|
const tagId$9 = "dsh-context/trendChart.css";
|
|
12026
12169
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$9) + "]") === null) {
|
|
12027
12170
|
const tag = document.createElement("style");
|
package/lib/index.js
CHANGED
|
@@ -11,11 +11,14 @@ import { randomUUID } from "node:crypto";
|
|
|
11
11
|
* The provider-id seam between dsh request envelopes and the models.dev
|
|
12
12
|
* registry: client/cost.ts resolves price-book branches through it, and
|
|
13
13
|
* host/fold.ts uses the DeepSeek resolution to split the session-cost totals
|
|
14
|
-
* into peak/off-peak periods. Only the renames live here —
|
|
15
|
-
*
|
|
14
|
+
* into peak/off-peak periods. Only the renames live here — a dsh route key
|
|
15
|
+
* that is not a registry id (user-declared gateways, `claude`/`codex`
|
|
16
|
+
* subscriptions, org-prefixed catalogs) passes through and prices through
|
|
17
|
+
* the model-side resolution index instead (client/cost.ts).
|
|
16
18
|
*/
|
|
17
19
|
const MODELS_DEV_PROVIDER_IDS = {
|
|
18
20
|
"deepseek-official": "deepseek",
|
|
21
|
+
"deepseek-account": "deepseek",
|
|
19
22
|
"kimi-coding": "moonshotai",
|
|
20
23
|
"minimax-cn": "minimax",
|
|
21
24
|
"zai-coding-cn": "zhipuai"
|
|
@@ -3176,7 +3179,7 @@ function createContextTimelineDefinition(config, slim) {
|
|
|
3176
3179
|
},
|
|
3177
3180
|
init: () => createTimelineState(),
|
|
3178
3181
|
apply: (state, event) => applyTimeline(state, event, bounds),
|
|
3179
|
-
stateVersion:
|
|
3182
|
+
stateVersion: 21
|
|
3180
3183
|
};
|
|
3181
3184
|
}
|
|
3182
3185
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-context",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "A DeepSeek Harness plugin for context insight and management, with context dashboard and context command, for understanding how the context is made of, and how it evolves.",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"author": "bowenliang123",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"@deepseek-ai/dsh-settings": "0.1.5-rc.1",
|
|
113
113
|
"@deepseek-ai/dsh-util-workspace-path": "0.1.5-rc.1",
|
|
114
114
|
"@deepseek-ai/schemastery": "^3.18.4",
|
|
115
|
-
"@opencode-ai/models": "^0.0.
|
|
115
|
+
"@opencode-ai/models": "^0.0.87",
|
|
116
116
|
"@shikijs/langs": "^4.3.1",
|
|
117
117
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
118
118
|
"@tailwindcss/node": "4.3.3",
|