dsh-usage-spend 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +76 -2
- package/README.zh.md +77 -0
- package/cordis.patch.yml +11 -0
- package/icon.svg +4 -0
- package/lib/client.js +1250 -0
- package/lib/index.js +20 -0
- package/locale/en.json +7 -0
- package/locale/zh.json +7 -0
- package/package.json +72 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,1250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-usage-cost — browser half.
|
|
3
|
+
*
|
|
4
|
+
* Contributes:
|
|
5
|
+
* - `conversation.composer.dock` id `usage` at priority -1, replacing the
|
|
6
|
+
* built-in token-usage pill with one that also shows estimated spend and
|
|
7
|
+
* keeps the cache-hit reading. Its dialog puts the amount right after the
|
|
8
|
+
* exact token count.
|
|
9
|
+
* - `conversation.chat.turnTail` id `turn-usage-cost`: a hidden anchor that
|
|
10
|
+
* rewrites the built-in per-turn "用量 N tok" action label to
|
|
11
|
+
* "N tok · ≈¥x.xx" (the line itself is gone).
|
|
12
|
+
* - `plugins.row.config` for this row, so the Plugins page exposes the
|
|
13
|
+
* currency and exchange-rate settings.
|
|
14
|
+
*
|
|
15
|
+
* Pricing is a local USD / 1M token table (models.dev snapshot) with an
|
|
16
|
+
* optional localStorage override. No network request is made for pricing.
|
|
17
|
+
*/
|
|
18
|
+
window.__ModuleLoader__.load({
|
|
19
|
+
id: "dsh-usage-cost",
|
|
20
|
+
factory: (require) => {
|
|
21
|
+
var module = { exports: {} };
|
|
22
|
+
var exports = module.exports;
|
|
23
|
+
|
|
24
|
+
const React = require("react");
|
|
25
|
+
const ReactDOM = require("react-dom");
|
|
26
|
+
const primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
27
|
+
const h = React.createElement;
|
|
28
|
+
|
|
29
|
+
const NS = "dsh-usage-cost";
|
|
30
|
+
const STYLE_ID = "dsh-usage-cost/styles";
|
|
31
|
+
const ENTRY_ID = "usage-cost";
|
|
32
|
+
const CONFIG_KEY = "dsh-usage-spend#usage-cost";
|
|
33
|
+
const CURRENCY_KEY = "dsh.cost.currency";
|
|
34
|
+
const RATE_KEY = "dsh.cost.usdToCny";
|
|
35
|
+
const PRICES_KEY = "dsh.cost.prices";
|
|
36
|
+
const DEFAULT_RATE = 7.2;
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------- locale
|
|
39
|
+
const zh = {
|
|
40
|
+
"pill.tokens": "{count} tok",
|
|
41
|
+
"pill.cacheHit": "缓存命中 {percent}%",
|
|
42
|
+
"dialog.title": "Token 用量",
|
|
43
|
+
"dialog.cacheHit": "缓存命中",
|
|
44
|
+
"dialog.input": "未缓存输入",
|
|
45
|
+
"dialog.cacheRead": "缓存读取",
|
|
46
|
+
"dialog.cacheWrite": "缓存写入",
|
|
47
|
+
"dialog.output": "输出",
|
|
48
|
+
"currency.cny": "¥ 人民币",
|
|
49
|
+
"currency.usd": "$ 美元",
|
|
50
|
+
"currency.both": "¥+$",
|
|
51
|
+
"config.summary": "在会话底部显示 token 用量与估算花费,可设置币种和汇率。",
|
|
52
|
+
"config.title": "用量与花费",
|
|
53
|
+
"config.currency": "显示币种",
|
|
54
|
+
"config.rate": "汇率:1 USD = ? CNY",
|
|
55
|
+
"config.rateHint": "只影响人民币估算,默认 7.2。",
|
|
56
|
+
"config.ratesTitle": "单价(USD / 1M tok)",
|
|
57
|
+
"config.ratesNote": "价格表是 models.dev 的 DeepSeek 快照,费用仅用于估算。",
|
|
58
|
+
"config.model": "模型",
|
|
59
|
+
"config.showBalance": "显示左侧栏余额",
|
|
60
|
+
"config.showBalanceHint": "关闭后不显示余额 chip,也不会请求账号余额。",
|
|
61
|
+
"balance.label": "余额",
|
|
62
|
+
"balance.loading": "读取中…",
|
|
63
|
+
"balance.unavailable": "暂不可用",
|
|
64
|
+
"balance.signedOut": "未登录",
|
|
65
|
+
"balance.refresh": "点击刷新余额",
|
|
66
|
+
"balance.tooltip": "充值余额 {wallets}",
|
|
67
|
+
"balance.bonus": "赠金 {wallets}",
|
|
68
|
+
"config.save": "保存",
|
|
69
|
+
"config.saving": "保存中…",
|
|
70
|
+
"config.saved": "已保存",
|
|
71
|
+
"config.failed": "保存失败,请检查取值后重试。",
|
|
72
|
+
"config.reset": "恢复默认",
|
|
73
|
+
"config.readOnly": "当前部署的设置为只读。",
|
|
74
|
+
"config.unavailable": "配置表单不可用(设置服务未加载)。",
|
|
75
|
+
"config.invalidRate": "汇率需为 1–100 之间的数字。",
|
|
76
|
+
};
|
|
77
|
+
const en = {
|
|
78
|
+
"pill.tokens": "{count} tok",
|
|
79
|
+
"pill.cacheHit": "cache hit {percent}%",
|
|
80
|
+
"dialog.title": "Token usage",
|
|
81
|
+
"dialog.cacheHit": "Cache hit",
|
|
82
|
+
"dialog.input": "Uncached input",
|
|
83
|
+
"dialog.cacheRead": "Cache read",
|
|
84
|
+
"dialog.cacheWrite": "Cache write",
|
|
85
|
+
"dialog.output": "Output",
|
|
86
|
+
"currency.cny": "¥ CNY",
|
|
87
|
+
"currency.usd": "$ USD",
|
|
88
|
+
"currency.both": "¥+$",
|
|
89
|
+
"config.summary": "Shows token usage and estimated spend under the composer, with currency and exchange-rate settings.",
|
|
90
|
+
"config.title": "Usage and spend",
|
|
91
|
+
"config.currency": "Display currency",
|
|
92
|
+
"config.rate": "Exchange rate: 1 USD = ? CNY",
|
|
93
|
+
"config.rateHint": "Only affects the CNY estimate; default 7.2.",
|
|
94
|
+
"config.ratesTitle": "Rates (USD / 1M tok)",
|
|
95
|
+
"config.ratesNote": "Rates are the models.dev DeepSeek snapshot; spend is an estimate.",
|
|
96
|
+
"config.model": "Model",
|
|
97
|
+
"config.showBalance": "Show the sidebar balance",
|
|
98
|
+
"config.showBalanceHint": "When off, the balance chip is hidden and no balance request is made.",
|
|
99
|
+
"balance.label": "Balance",
|
|
100
|
+
"balance.loading": "Loading…",
|
|
101
|
+
"balance.unavailable": "Unavailable",
|
|
102
|
+
"balance.signedOut": "Not signed in",
|
|
103
|
+
"balance.refresh": "Click to refresh the balance",
|
|
104
|
+
"balance.tooltip": "Credit balance {wallets}",
|
|
105
|
+
"balance.bonus": "Bonus {wallets}",
|
|
106
|
+
"config.save": "Save",
|
|
107
|
+
"config.saving": "Saving…",
|
|
108
|
+
"config.saved": "Saved",
|
|
109
|
+
"config.failed": "The values were not accepted; check them and retry.",
|
|
110
|
+
"config.reset": "Reset to default",
|
|
111
|
+
"config.readOnly": "This deployment stores settings read-only.",
|
|
112
|
+
"config.unavailable": "The configuration form is unavailable (settings service not loaded).",
|
|
113
|
+
"config.invalidRate": "Enter an exchange rate between 1 and 100.",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ----------------------------------------------------------------- styles
|
|
117
|
+
const CSS = [
|
|
118
|
+
".dshuc_anchor{min-width:0;font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));display:inline-flex}",
|
|
119
|
+
".dshuc_pill{box-sizing:border-box;border-radius:var(--dsw-radius-sm);max-width:100%;color:var(--dsw-alias-label-tertiary);font:inherit;font-variant-numeric:tabular-nums;line-height:inherit;white-space:nowrap;background:0 0;border:none;align-items:center;gap:6px;padding:1px 8px;display:inline-flex}",
|
|
120
|
+
".dshuc_pill svg{flex:none;width:14px;height:14px}",
|
|
121
|
+
"button.dshuc_pill{cursor:pointer}",
|
|
122
|
+
"button.dshuc_pill[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
|
|
123
|
+
"@media (hover:hover){button.dshuc_pill:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}}",
|
|
124
|
+
".dshuc_label{text-overflow:ellipsis;min-width:0;overflow:hidden}",
|
|
125
|
+
".dshuc_sep{color:var(--dsw-alias-separator-primary);margin:0 6px}",
|
|
126
|
+
".dshuc_money{color:var(--dsw-alias-label-secondary)}",
|
|
127
|
+
".dshuc_panel{z-index:1100;box-sizing:border-box;width:max-content;min-width:min(300px,calc(100vw - 24px),var(--dsh-anchored-max-width,100vw));max-width:min(440px,calc(100vw - 24px),var(--dsh-anchored-max-width,100vw));border-radius:var(--dsw-radius-lg);background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-secondary);cursor:default;max-height:var(--dsh-anchored-max-height,none);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;padding:16px;font-size:12px;line-height:18px;position:fixed;overflow:hidden auto}",
|
|
128
|
+
".dshuc_title{color:var(--dsw-alias-label-primary);justify-content:space-between;gap:16px;margin-bottom:8px;font-weight:500;display:flex}",
|
|
129
|
+
".dshuc_titleRule{border-top:.5px solid var(--dsw-alias-border-l2);margin-bottom:10px}",
|
|
130
|
+
".dshuc_titleValue{font-variant-numeric:tabular-nums;white-space:nowrap;min-width:0;overflow:hidden;text-overflow:ellipsis}",
|
|
131
|
+
".dshuc_titleLabel{align-items:center;gap:6px;min-width:0;display:inline-flex}",
|
|
132
|
+
".dshuc_titleLabel svg{flex:none;width:14px;height:14px}",
|
|
133
|
+
".dshuc_details{color:var(--dsw-alias-label-tertiary);grid-template-columns:minmax(76px,auto) minmax(0,1fr);gap:6px 16px;margin:0;display:grid}",
|
|
134
|
+
".dshuc_details dt,.dshuc_details dd{min-width:0;margin:0}",
|
|
135
|
+
".dshuc_details dd{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;text-align:right}",
|
|
136
|
+
".dshuc_cfg{display:flex;flex-direction:column;gap:14px;max-width:560px}",
|
|
137
|
+
".dshuc_cfgTitle{color:var(--dsw-alias-label-primary);font-weight:500;font-size:13px;line-height:20px}",
|
|
138
|
+
".dshuc_field{display:flex;flex-direction:column;gap:4px}",
|
|
139
|
+
".dshuc_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}",
|
|
140
|
+
".dshuc_toggleRow{display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:pointer}",
|
|
141
|
+
".dshuc_input{box-sizing:border-box;width:100%;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-l1,transparent);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;padding:5px 8px;outline:0}",
|
|
142
|
+
".dshuc_input:focus{border-color:var(--dsw-alias-label-accent,#6aa9ff)}",
|
|
143
|
+
".dshuc_hint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}",
|
|
144
|
+
".dshuc_actions{display:flex;align-items:center;gap:8px}",
|
|
145
|
+
".dshuc_status{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}",
|
|
146
|
+
".dshuc_status[data-tone=error]{color:var(--dsw-alias-state-error-primary,#f2a1a1)}",
|
|
147
|
+
".dshuc_cfgNote{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}",
|
|
148
|
+
".dshuc_table{border-collapse:collapse;width:100%;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
|
|
149
|
+
".dshuc_table th{color:var(--dsw-alias-label-tertiary);font-weight:400;text-align:right;padding:4px 6px;border-bottom:.5px solid var(--dsw-alias-border-l2)}",
|
|
150
|
+
".dshuc_table th:first-child{text-align:left}",
|
|
151
|
+
".dshuc_table td{color:var(--dsw-alias-label-secondary);text-align:right;padding:4px 6px;border-bottom:.5px solid var(--dsw-alias-border-l2)}",
|
|
152
|
+
".dshuc_table td:first-child{text-align:left;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
|
|
153
|
+
".dshuc_table tr:last-child td{border-bottom:0}",
|
|
154
|
+
".dshuc_balance{box-sizing:border-box;display:flex;align-items:center;gap:6px;width:100%;height:32px;padding:0 8px;border:0;border-radius:var(--dsw-radius-md);background:0 0;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;line-height:18px;white-space:nowrap;cursor:pointer;overflow:hidden}",
|
|
155
|
+
"@media (hover:hover){.dshuc_balance:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}}",
|
|
156
|
+
".dshuc_balanceGlyph{flex:none;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-bg-skeleton);font-size:10px;line-height:16px}",
|
|
157
|
+
".dshuc_balanceLabel{text-overflow:ellipsis;overflow:hidden;min-width:0}",
|
|
158
|
+
".dshuc_balanceValue{margin-left:auto;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;overflow:hidden;text-overflow:ellipsis}",
|
|
159
|
+
".dshuc_balance[data-wide=false]{width:36px;height:36px;justify-content:center;padding:0;gap:0}",
|
|
160
|
+
".dshuc_balance[data-wide=false] .dshuc_balanceLabel,.dshuc_balance[data-wide=false] .dshuc_balanceGlyph{display:none}",
|
|
161
|
+
".dshuc_balance[data-wide=false] .dshuc_balanceValue{margin:0;font-size:10px}",
|
|
162
|
+
"[class*='footerActions']:has(.dshuc_balance){flex-wrap:wrap}",
|
|
163
|
+
].join("");
|
|
164
|
+
|
|
165
|
+
function injectStyles() {
|
|
166
|
+
if (typeof document === "undefined") return;
|
|
167
|
+
if (document.querySelector('style[data-plugin-css="' + STYLE_ID + '"]') !== null) return;
|
|
168
|
+
const tag = document.createElement("style");
|
|
169
|
+
tag.dataset.plugin = "dsh-usage-cost";
|
|
170
|
+
tag.dataset.pluginCss = STYLE_ID;
|
|
171
|
+
tag.textContent = CSS;
|
|
172
|
+
document.head.appendChild(tag);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------- storage
|
|
176
|
+
function readStorage(key) {
|
|
177
|
+
try {
|
|
178
|
+
return window.localStorage.getItem(key);
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function writeStorage(key, value) {
|
|
184
|
+
try {
|
|
185
|
+
window.localStorage.setItem(key, value);
|
|
186
|
+
} catch {
|
|
187
|
+
/* storage may be unavailable */
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function readCurrency() {
|
|
191
|
+
const value = readStorage(CURRENCY_KEY);
|
|
192
|
+
return value === "USD" || value === "BOTH" || value === "CNY" ? value : "CNY";
|
|
193
|
+
}
|
|
194
|
+
function readRate() {
|
|
195
|
+
const raw = Number(readStorage(RATE_KEY));
|
|
196
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_RATE;
|
|
197
|
+
}
|
|
198
|
+
function sanitizeCurrency(value) {
|
|
199
|
+
return value === "USD" || value === "BOTH" ? value : "CNY";
|
|
200
|
+
}
|
|
201
|
+
function sanitizeRate(value) {
|
|
202
|
+
const rate = Number(value);
|
|
203
|
+
return Number.isFinite(rate) && rate > 0 ? rate : DEFAULT_RATE;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ------------------------------------------------------------ config store
|
|
207
|
+
/**
|
|
208
|
+
* Live values from the Host settings form, with localStorage as the fallback
|
|
209
|
+
* until the settings mirror answers (or when the service is not composed).
|
|
210
|
+
*/
|
|
211
|
+
function createConfigStore(entryId, fallback) {
|
|
212
|
+
let form = null;
|
|
213
|
+
let off = null;
|
|
214
|
+
let current = null;
|
|
215
|
+
const listeners = new Set();
|
|
216
|
+
|
|
217
|
+
function build() {
|
|
218
|
+
let value = null;
|
|
219
|
+
let writable = false;
|
|
220
|
+
let ready = false;
|
|
221
|
+
if (form !== null) {
|
|
222
|
+
try {
|
|
223
|
+
const snapshot = form.getSnapshot();
|
|
224
|
+
if (snapshot !== undefined && snapshot.status === "ready" && snapshot.value !== undefined) {
|
|
225
|
+
value = snapshot.value;
|
|
226
|
+
writable = snapshot.writable !== false;
|
|
227
|
+
ready = true;
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
/* keep the fallback */
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const base = fallback();
|
|
234
|
+
return Object.freeze({
|
|
235
|
+
currency: sanitizeCurrency(value?.currency ?? base.currency),
|
|
236
|
+
usdToCny: sanitizeRate(value?.usdToCny ?? base.usdToCny),
|
|
237
|
+
showBalance: value?.showBalance !== false,
|
|
238
|
+
ready,
|
|
239
|
+
writable,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function refresh() {
|
|
243
|
+
current = build();
|
|
244
|
+
for (const listener of listeners) {
|
|
245
|
+
try {
|
|
246
|
+
listener();
|
|
247
|
+
} catch {
|
|
248
|
+
/* a listener must not break the store */
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
current = build();
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
get: () => current,
|
|
256
|
+
subscribe(listener) {
|
|
257
|
+
listeners.add(listener);
|
|
258
|
+
return () => {
|
|
259
|
+
listeners.delete(listener);
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
attach(next) {
|
|
263
|
+
if (off !== null) {
|
|
264
|
+
try {
|
|
265
|
+
off();
|
|
266
|
+
} catch {
|
|
267
|
+
/* the previous form may already be disposed */
|
|
268
|
+
}
|
|
269
|
+
off = null;
|
|
270
|
+
}
|
|
271
|
+
form = next;
|
|
272
|
+
if (next !== null && typeof next.subscribe === "function") off = next.subscribe(refresh);
|
|
273
|
+
refresh();
|
|
274
|
+
return () => {
|
|
275
|
+
if (form === next) form = null;
|
|
276
|
+
if (off !== null) {
|
|
277
|
+
off();
|
|
278
|
+
off = null;
|
|
279
|
+
}
|
|
280
|
+
refresh();
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
async setCurrency(value) {
|
|
284
|
+
const next = sanitizeCurrency(value);
|
|
285
|
+
if (form !== null && current.ready && current.writable) {
|
|
286
|
+
try {
|
|
287
|
+
if (await form.set("currency", next)) {
|
|
288
|
+
refresh();
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
/* fall through to the local override */
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
writeStorage(CURRENCY_KEY, next);
|
|
296
|
+
refresh();
|
|
297
|
+
return true;
|
|
298
|
+
},
|
|
299
|
+
async setRate(value) {
|
|
300
|
+
const next = sanitizeRate(value);
|
|
301
|
+
if (form !== null && current.ready && current.writable) {
|
|
302
|
+
try {
|
|
303
|
+
if (await form.set("usdToCny", next)) {
|
|
304
|
+
refresh();
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
/* fall through to the local override */
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
writeStorage(RATE_KEY, String(next));
|
|
312
|
+
refresh();
|
|
313
|
+
return true;
|
|
314
|
+
},
|
|
315
|
+
entryId,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const configStore = createConfigStore(ENTRY_ID, () => ({ currency: readCurrency(), usdToCny: readRate() }));
|
|
320
|
+
function useConfig() {
|
|
321
|
+
return React.useSyncExternalStore(configStore.subscribe, configStore.get, configStore.get);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function attachConfig(ctx) {
|
|
325
|
+
if (typeof ctx.inject !== "function") return;
|
|
326
|
+
try {
|
|
327
|
+
ctx.inject(["configForms"], (scope) => {
|
|
328
|
+
let forms = scope.configForms;
|
|
329
|
+
if (forms === undefined && typeof scope.get === "function") forms = scope.get("configForms");
|
|
330
|
+
if (forms === undefined || typeof forms.get !== "function") return;
|
|
331
|
+
const detach = configStore.attach(forms.get(ENTRY_ID));
|
|
332
|
+
if (typeof scope.effect === "function") {
|
|
333
|
+
scope.effect(() => detach, "dsh-usage-cost: config form");
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
} catch {
|
|
337
|
+
/* the fallback store keeps working */
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ----------------------------------------------------------------- prices
|
|
342
|
+
/** USD per 1M tokens, models.dev snapshot (2026-11-07). */
|
|
343
|
+
const MODEL_PRICES = {
|
|
344
|
+
"deepseek-flash": { input: 0.15, cacheRead: 0.003, cacheWrite: 0.15, output: 0.6 },
|
|
345
|
+
"deepseek-v4-flash": { input: 0.15, cacheRead: 0.003, cacheWrite: 0.15, output: 0.6 },
|
|
346
|
+
"deepseek-v4-flash-vision-exp": { input: 0.15, cacheRead: 0.003, cacheWrite: 0.15, output: 0.6 },
|
|
347
|
+
"deepseek-v4-pro": { input: 0.66, cacheRead: 0.022, cacheWrite: 0.66, output: 1.98 },
|
|
348
|
+
"deepseek-chat": { input: 0.28, cacheRead: 0.028, cacheWrite: 0.28, output: 0.42 },
|
|
349
|
+
"deepseek-reasoner": { input: 0.28, cacheRead: 0.028, cacheWrite: 0.28, output: 0.42 },
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const PRICE_OVERRIDES = (() => {
|
|
353
|
+
const raw = readStorage(PRICES_KEY);
|
|
354
|
+
if (raw === null) return {};
|
|
355
|
+
try {
|
|
356
|
+
const parsed = JSON.parse(raw);
|
|
357
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
358
|
+
} catch {
|
|
359
|
+
return {};
|
|
360
|
+
}
|
|
361
|
+
})();
|
|
362
|
+
|
|
363
|
+
function normalizeRates(value) {
|
|
364
|
+
if (value === null || typeof value !== "object") return null;
|
|
365
|
+
const input = Number(value.input);
|
|
366
|
+
const output = Number(value.output);
|
|
367
|
+
const cacheRead = Number(value.cacheRead);
|
|
368
|
+
const cacheWrite = Number(value.cacheWrite);
|
|
369
|
+
if (!Number.isFinite(input) || !Number.isFinite(output) || input < 0 || output < 0) return null;
|
|
370
|
+
return {
|
|
371
|
+
input,
|
|
372
|
+
output,
|
|
373
|
+
cacheRead: Number.isFinite(cacheRead) && cacheRead >= 0 ? cacheRead : input,
|
|
374
|
+
cacheWrite: Number.isFinite(cacheWrite) && cacheWrite >= 0 ? cacheWrite : input,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function priceFor(route) {
|
|
379
|
+
if (route === null || route === undefined || typeof route.model !== "string" || route.model === "") return null;
|
|
380
|
+
const model = route.model;
|
|
381
|
+
const bare = model.slice(model.lastIndexOf("/") + 1);
|
|
382
|
+
const candidates = [model, route.provider + "/" + model, bare];
|
|
383
|
+
for (const key of candidates) {
|
|
384
|
+
const override = PRICE_OVERRIDES[key];
|
|
385
|
+
if (override !== undefined) {
|
|
386
|
+
const rates = normalizeRates(override);
|
|
387
|
+
if (rates !== null) return { rates, custom: true };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const table = MODEL_PRICES[bare] ?? MODEL_PRICES[model];
|
|
391
|
+
if (table === undefined) return null;
|
|
392
|
+
const rates = normalizeRates(table);
|
|
393
|
+
return rates === null ? null : { rates, custom: false };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ------------------------------------------------------------------ money
|
|
397
|
+
function num(value) {
|
|
398
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
399
|
+
}
|
|
400
|
+
function billedInput(usage) {
|
|
401
|
+
return num(usage.uncachedInputTokens) + num(usage.cacheReadTokens) + num(usage.cacheWriteTokens);
|
|
402
|
+
}
|
|
403
|
+
function totalTokens(usage) {
|
|
404
|
+
return billedInput(usage) + num(usage.outputTokens);
|
|
405
|
+
}
|
|
406
|
+
function costOf(usage, rates) {
|
|
407
|
+
if (rates === null) return null;
|
|
408
|
+
const usd =
|
|
409
|
+
(num(usage.uncachedInputTokens) * rates.input +
|
|
410
|
+
num(usage.cacheReadTokens) * rates.cacheRead +
|
|
411
|
+
num(usage.cacheWriteTokens) * rates.cacheWrite +
|
|
412
|
+
num(usage.outputTokens) * rates.output) /
|
|
413
|
+
1e6;
|
|
414
|
+
return Number.isFinite(usd) ? usd : null;
|
|
415
|
+
}
|
|
416
|
+
function cacheHitPercent(usage) {
|
|
417
|
+
const prompt = billedInput(usage);
|
|
418
|
+
if (prompt === 0) return null;
|
|
419
|
+
if (num(usage.cacheReadTokens) >= prompt) return "100";
|
|
420
|
+
const percent = (num(usage.cacheReadTokens) / prompt) * 100;
|
|
421
|
+
const rounded = Math.round(percent * 10) / 10;
|
|
422
|
+
if (rounded >= 100) return "99.9";
|
|
423
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
424
|
+
}
|
|
425
|
+
function formatTokens(value) {
|
|
426
|
+
if (!Number.isFinite(value) || value <= 0) return "0";
|
|
427
|
+
if (value < 1000) return String(Math.round(value));
|
|
428
|
+
if (value < 1e6) return (value / 1e3).toFixed(1) + "K";
|
|
429
|
+
return (value / 1e6).toFixed(1) + "M";
|
|
430
|
+
}
|
|
431
|
+
function exactCount(value) {
|
|
432
|
+
return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
433
|
+
}
|
|
434
|
+
function amount(value) {
|
|
435
|
+
if (!Number.isFinite(value)) return "0";
|
|
436
|
+
if (value >= 100) return value.toFixed(0);
|
|
437
|
+
if (value >= 1) return value.toFixed(2);
|
|
438
|
+
if (value >= 0.01) return value.toFixed(3);
|
|
439
|
+
return value.toFixed(4);
|
|
440
|
+
}
|
|
441
|
+
function moneyText(usd, currency, rate, spaced) {
|
|
442
|
+
if (usd === null) return null;
|
|
443
|
+
const gap = spaced === true ? " " : "";
|
|
444
|
+
const cny = "≈" + gap + "¥" + amount(usd * (Number.isFinite(rate) && rate > 0 ? rate : readRate()));
|
|
445
|
+
const dollar = "≈" + gap + "$" + amount(usd);
|
|
446
|
+
if (currency === "USD") return dollar;
|
|
447
|
+
if (currency === "BOTH") return cny + " / " + dollar;
|
|
448
|
+
return cny;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// --------------------------------------------------------------- balance
|
|
452
|
+
function moneySymbol(currency) {
|
|
453
|
+
return currency === "USD" ? "$" : "¥";
|
|
454
|
+
}
|
|
455
|
+
function groupDigits(text) {
|
|
456
|
+
return text.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
457
|
+
}
|
|
458
|
+
/** Two decimals with grouping; tiny positive amounts read as `<0.01`. */
|
|
459
|
+
function formatMoney(value, currency) {
|
|
460
|
+
const symbol = moneySymbol(currency);
|
|
461
|
+
const number = Number(value);
|
|
462
|
+
if (!Number.isFinite(number)) return symbol + "0.00";
|
|
463
|
+
if (number === 0) return symbol + "0.00";
|
|
464
|
+
if (number < 0) {
|
|
465
|
+
const absolute = Math.abs(number);
|
|
466
|
+
return "-" + symbol + (absolute < 0.01 ? "0.01" : groupDigits(absolute.toFixed(2)));
|
|
467
|
+
}
|
|
468
|
+
if (number < 0.01) return symbol + "<0.01";
|
|
469
|
+
return symbol + groupDigits(number.toFixed(2));
|
|
470
|
+
}
|
|
471
|
+
/** Approximation that fits the 36px collapsed rail. */
|
|
472
|
+
function compactMoney(value, currency) {
|
|
473
|
+
const symbol = moneySymbol(currency);
|
|
474
|
+
const number = Number(value);
|
|
475
|
+
if (!Number.isFinite(number) || number === 0) return symbol + "0";
|
|
476
|
+
const sign = number < 0 ? "-" : "";
|
|
477
|
+
const absolute = Math.abs(number);
|
|
478
|
+
if (absolute >= 1000) return sign + symbol + (absolute / 1000).toFixed(1).replace(/\.0$/, "") + "k";
|
|
479
|
+
if (absolute >= 1) return sign + symbol + String(Math.round(absolute));
|
|
480
|
+
return sign + symbol + "<1";
|
|
481
|
+
}
|
|
482
|
+
function walletListText(wallets) {
|
|
483
|
+
return wallets.map((wallet) => formatMoney(wallet.balance, wallet.currency)).join(" / ");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function createBalanceStore(ctx) {
|
|
487
|
+
let state = Object.freeze({ status: "idle", wallets: Object.freeze([]), bonusWallets: Object.freeze([]), error: null });
|
|
488
|
+
const listeners = new Set();
|
|
489
|
+
let timer = null;
|
|
490
|
+
let retryTimer = null;
|
|
491
|
+
let running = false;
|
|
492
|
+
let generation = 0;
|
|
493
|
+
const retrySoon = (delayMs) => {
|
|
494
|
+
if (!running || retryTimer !== null) return;
|
|
495
|
+
retryTimer = setTimeout(() => {
|
|
496
|
+
retryTimer = null;
|
|
497
|
+
void refresh();
|
|
498
|
+
}, delayMs);
|
|
499
|
+
if (retryTimer !== null && typeof retryTimer === "object" && typeof retryTimer.unref === "function") retryTimer.unref();
|
|
500
|
+
};
|
|
501
|
+
const publish = (patch) => {
|
|
502
|
+
state = Object.freeze({ ...state, ...patch });
|
|
503
|
+
for (const listener of listeners) {
|
|
504
|
+
try {
|
|
505
|
+
listener();
|
|
506
|
+
} catch {
|
|
507
|
+
/* a subscriber must not break the balance chip */
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
const clientMeta = () => {
|
|
512
|
+
let locale = "zh";
|
|
513
|
+
try {
|
|
514
|
+
locale = ctx.locale.getSnapshot().active;
|
|
515
|
+
} catch {
|
|
516
|
+
/* keep the default */
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
version: "0.2.2-rc.1",
|
|
520
|
+
locale,
|
|
521
|
+
timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60,
|
|
522
|
+
};
|
|
523
|
+
};
|
|
524
|
+
const refresh = async () => {
|
|
525
|
+
const generationAtStart = ++generation;
|
|
526
|
+
if (state.status === "idle") publish({ status: "loading", error: null });
|
|
527
|
+
try {
|
|
528
|
+
const remote = ctx.remote;
|
|
529
|
+
const account =
|
|
530
|
+
remote !== undefined && remote !== null && remote.account !== undefined
|
|
531
|
+
? remote.account
|
|
532
|
+
: typeof ctx.get === "function"
|
|
533
|
+
? ctx.get("remote.account")
|
|
534
|
+
: undefined;
|
|
535
|
+
if (account === undefined || account === null || typeof account.getBalance !== "function") {
|
|
536
|
+
publish({ status: "loading", error: "account remote not mounted" });
|
|
537
|
+
retrySoon(1000);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const result = await account.getBalance(clientMeta());
|
|
541
|
+
if (generationAtStart !== generation) return;
|
|
542
|
+
if (result === null || result === undefined || result.ok !== true) {
|
|
543
|
+
publish({ status: "unavailable", error: result?.error?.code ?? "balance request failed" });
|
|
544
|
+
retrySoon(5000);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const value = result.value;
|
|
548
|
+
if (value === null || value === undefined) {
|
|
549
|
+
publish({ status: "signedOut", wallets: Object.freeze([]), bonusWallets: Object.freeze([]), error: "account grant unavailable" });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (value.status !== "ready") {
|
|
553
|
+
publish({ status: "unavailable", error: "balance " + String(value.status) });
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
publish({
|
|
557
|
+
status: "ready",
|
|
558
|
+
wallets: Object.freeze(Array.isArray(value.value) ? value.value : []),
|
|
559
|
+
bonusWallets: Object.freeze(Array.isArray(value.bonusWallets) ? value.bonusWallets : []),
|
|
560
|
+
error: null,
|
|
561
|
+
});
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (generationAtStart !== generation) return;
|
|
564
|
+
publish({ status: "unavailable", error: error instanceof Error ? error.message : String(error) });
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
const onVisible = () => {
|
|
568
|
+
if (typeof document !== "undefined" && document.visibilityState === "visible") void refresh();
|
|
569
|
+
};
|
|
570
|
+
return {
|
|
571
|
+
getSnapshot: () => state,
|
|
572
|
+
subscribe(listener) {
|
|
573
|
+
listeners.add(listener);
|
|
574
|
+
return () => {
|
|
575
|
+
listeners.delete(listener);
|
|
576
|
+
};
|
|
577
|
+
},
|
|
578
|
+
refresh,
|
|
579
|
+
start() {
|
|
580
|
+
if (running) return;
|
|
581
|
+
running = true;
|
|
582
|
+
void refresh();
|
|
583
|
+
timer = setInterval(() => {
|
|
584
|
+
void refresh();
|
|
585
|
+
}, 60000);
|
|
586
|
+
if (timer !== null && typeof timer === "object" && typeof timer.unref === "function") timer.unref();
|
|
587
|
+
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisible);
|
|
588
|
+
},
|
|
589
|
+
stop() {
|
|
590
|
+
running = false;
|
|
591
|
+
generation += 1;
|
|
592
|
+
if (timer !== null) {
|
|
593
|
+
clearInterval(timer);
|
|
594
|
+
timer = null;
|
|
595
|
+
}
|
|
596
|
+
if (retryTimer !== null) {
|
|
597
|
+
clearTimeout(retryTimer);
|
|
598
|
+
retryTimer = null;
|
|
599
|
+
}
|
|
600
|
+
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function BalanceChip({ wide, balance, t }) {
|
|
606
|
+
const config = useConfig();
|
|
607
|
+
const state = React.useSyncExternalStore(balance.subscribe, balance.getSnapshot, balance.getSnapshot);
|
|
608
|
+
if (!config.showBalance) return null;
|
|
609
|
+
const wallets = state.wallets ?? [];
|
|
610
|
+
const bonusWallets = state.bonusWallets ?? [];
|
|
611
|
+
const symbol = wallets[0]?.currency === "USD" ? "$" : "¥";
|
|
612
|
+
let value;
|
|
613
|
+
if (state.status === "ready") {
|
|
614
|
+
value =
|
|
615
|
+
wallets.length === 0
|
|
616
|
+
? formatMoney(0, "CNY")
|
|
617
|
+
: wide
|
|
618
|
+
? walletListText(wallets)
|
|
619
|
+
: compactMoney(wallets[0].balance, wallets[0].currency);
|
|
620
|
+
} else if (state.status === "signedOut") {
|
|
621
|
+
value = wide ? t("balance.signedOut") : "--";
|
|
622
|
+
} else if (state.status === "unavailable") {
|
|
623
|
+
value = "--";
|
|
624
|
+
} else {
|
|
625
|
+
value = "…";
|
|
626
|
+
}
|
|
627
|
+
const tooltip =
|
|
628
|
+
state.status === "ready"
|
|
629
|
+
? [
|
|
630
|
+
t("balance.tooltip", { wallets: wallets.length > 0 ? walletListText(wallets) : formatMoney(0, "CNY") }),
|
|
631
|
+
bonusWallets.length > 0 ? t("balance.bonus", { wallets: walletListText(bonusWallets) }) : null,
|
|
632
|
+
t("balance.refresh"),
|
|
633
|
+
]
|
|
634
|
+
.filter((part) => part !== null)
|
|
635
|
+
.join(" · ")
|
|
636
|
+
: state.status === "signedOut"
|
|
637
|
+
? t("balance.signedOut") + " · " + t("balance.refresh")
|
|
638
|
+
: state.status === "unavailable"
|
|
639
|
+
? t("balance.unavailable") + (state.error ? "(" + state.error + ")" : "") + " · " + t("balance.refresh")
|
|
640
|
+
: t("balance.loading");
|
|
641
|
+
return h(
|
|
642
|
+
primitives.Tooltip,
|
|
643
|
+
{
|
|
644
|
+
label: tooltip,
|
|
645
|
+
side: "top",
|
|
646
|
+
children: h(
|
|
647
|
+
"button",
|
|
648
|
+
{
|
|
649
|
+
type: "button",
|
|
650
|
+
className: "dshuc_balance",
|
|
651
|
+
"data-wide": wide,
|
|
652
|
+
"aria-label": tooltip,
|
|
653
|
+
onClick: () => {
|
|
654
|
+
void balance.refresh();
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
h("span", { className: "dshuc_balanceGlyph", "aria-hidden": true }, symbol),
|
|
658
|
+
h("span", { className: "dshuc_balanceLabel" }, t("balance.label")),
|
|
659
|
+
h("span", { className: "dshuc_balanceValue" }, value),
|
|
660
|
+
),
|
|
661
|
+
},
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function attachBalance(ctx) {
|
|
666
|
+
if (typeof ctx.inject !== "function") return;
|
|
667
|
+
try {
|
|
668
|
+
ctx.inject(["remote", "remote.account", "remoteAccess"], (scope) => {
|
|
669
|
+
const balance = createBalanceStore(scope);
|
|
670
|
+
let unsubscribe = null;
|
|
671
|
+
const register = () => {
|
|
672
|
+
const sync = () => {
|
|
673
|
+
if (configStore.get().showBalance) balance.start();
|
|
674
|
+
else balance.stop();
|
|
675
|
+
};
|
|
676
|
+
unsubscribe = configStore.subscribe(sync);
|
|
677
|
+
sync();
|
|
678
|
+
const slots = scope.slots ?? (typeof scope.get === "function" ? scope.get("slots") : undefined);
|
|
679
|
+
if (slots === undefined || typeof slots.inject !== "function") {
|
|
680
|
+
return () => {
|
|
681
|
+
unsubscribe?.();
|
|
682
|
+
unsubscribe = null;
|
|
683
|
+
balance.stop();
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
const off = slots.inject("sidebar.footer.action", () =>
|
|
687
|
+
slots.register(
|
|
688
|
+
{
|
|
689
|
+
name: "sidebar.footer.action",
|
|
690
|
+
id: "usage-cost-balance",
|
|
691
|
+
order: 10,
|
|
692
|
+
locale: NS,
|
|
693
|
+
inject: () => ({ balance }),
|
|
694
|
+
},
|
|
695
|
+
BalanceChip,
|
|
696
|
+
),
|
|
697
|
+
);
|
|
698
|
+
return () => {
|
|
699
|
+
unsubscribe?.();
|
|
700
|
+
unsubscribe = null;
|
|
701
|
+
balance.stop();
|
|
702
|
+
off?.();
|
|
703
|
+
};
|
|
704
|
+
};
|
|
705
|
+
const access = scope.remoteAccess;
|
|
706
|
+
if (access !== undefined && typeof access.whenAccess === "function") {
|
|
707
|
+
scope.effect(() => access.whenAccess("account/watch", register), "dsh-usage-cost: balance");
|
|
708
|
+
} else {
|
|
709
|
+
scope.effect(() => register(), "dsh-usage-cost: balance");
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
} catch {
|
|
713
|
+
/* the balance chip is best effort */
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// -------------------------------------------------------------- components
|
|
718
|
+
const MEASURE_STYLE = { visibility: "hidden", left: 0, top: 0 };
|
|
719
|
+
|
|
720
|
+
function DialogPill(props) {
|
|
721
|
+
const { stat, icon, label, text, title, titleValue, children } = props;
|
|
722
|
+
const [open, setOpen] = React.useState(false);
|
|
723
|
+
const rootRef = React.useRef(null);
|
|
724
|
+
const panelRef = React.useRef(null);
|
|
725
|
+
const pos = primitives.useAnchoredPosition({
|
|
726
|
+
open,
|
|
727
|
+
anchorRef: rootRef,
|
|
728
|
+
panelRef,
|
|
729
|
+
side: "top",
|
|
730
|
+
gap: 8,
|
|
731
|
+
margin: 12,
|
|
732
|
+
});
|
|
733
|
+
primitives.useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef);
|
|
734
|
+
React.useEffect(() => {
|
|
735
|
+
if (!open) return;
|
|
736
|
+
const onKeyDown = (event) => {
|
|
737
|
+
if (event.key === "Escape") setOpen(false);
|
|
738
|
+
};
|
|
739
|
+
document.addEventListener("keydown", onKeyDown);
|
|
740
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
741
|
+
}, [open]);
|
|
742
|
+
return h(
|
|
743
|
+
"span",
|
|
744
|
+
{ ref: rootRef, className: "dshuc_anchor", "data-composer-stat": stat },
|
|
745
|
+
h(
|
|
746
|
+
primitives.Tooltip,
|
|
747
|
+
{
|
|
748
|
+
label: text,
|
|
749
|
+
side: "top",
|
|
750
|
+
portal: true,
|
|
751
|
+
delayMs: 200,
|
|
752
|
+
onlyWhenClipped: true,
|
|
753
|
+
disabled: open,
|
|
754
|
+
},
|
|
755
|
+
h(
|
|
756
|
+
"button",
|
|
757
|
+
{
|
|
758
|
+
type: "button",
|
|
759
|
+
className: "dshuc_pill",
|
|
760
|
+
"aria-haspopup": "dialog",
|
|
761
|
+
"aria-expanded": open,
|
|
762
|
+
"aria-label": text,
|
|
763
|
+
onClick: () => setOpen(!open),
|
|
764
|
+
},
|
|
765
|
+
icon,
|
|
766
|
+
h("span", { className: "dshuc_label" }, label),
|
|
767
|
+
),
|
|
768
|
+
),
|
|
769
|
+
open &&
|
|
770
|
+
ReactDOM.createPortal(
|
|
771
|
+
h(
|
|
772
|
+
"div",
|
|
773
|
+
{
|
|
774
|
+
ref: panelRef,
|
|
775
|
+
className: "dshuc_panel",
|
|
776
|
+
role: "dialog",
|
|
777
|
+
"aria-label": title,
|
|
778
|
+
style: pos ?? MEASURE_STYLE,
|
|
779
|
+
},
|
|
780
|
+
h(
|
|
781
|
+
"div",
|
|
782
|
+
{ className: "dshuc_title" },
|
|
783
|
+
h("span", { className: "dshuc_titleLabel" }, icon, title),
|
|
784
|
+
titleValue !== undefined ? h("span", { className: "dshuc_titleValue" }, titleValue) : null,
|
|
785
|
+
),
|
|
786
|
+
h("div", { className: "dshuc_titleRule", "aria-hidden": true }),
|
|
787
|
+
children,
|
|
788
|
+
),
|
|
789
|
+
document.body,
|
|
790
|
+
),
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function SessionUsageCostPill({ useProjection, t }) {
|
|
795
|
+
const usage = useProjection("tokenUsage");
|
|
796
|
+
const selection = useProjection("modelSelection");
|
|
797
|
+
const config = useConfig();
|
|
798
|
+
if (usage === undefined) return null;
|
|
799
|
+
const prompt = billedInput(usage);
|
|
800
|
+
const output = num(usage.outputTokens);
|
|
801
|
+
if (prompt === 0 && output === 0) return null;
|
|
802
|
+
|
|
803
|
+
const route = selection?.pending ?? selection?.lastUsed ?? null;
|
|
804
|
+
const priced = priceFor(route);
|
|
805
|
+
const usd = priced === null ? null : costOf(usage, priced.rates);
|
|
806
|
+
const total = prompt + output;
|
|
807
|
+
const cacheHit = cacheHitPercent(usage);
|
|
808
|
+
const money = moneyText(usd, config.currency, config.usdToCny);
|
|
809
|
+
const titleMoney = moneyText(usd, config.currency, config.usdToCny, true);
|
|
810
|
+
const moneyNode = money === null ? null : h("span", { className: "dshuc_money" }, money);
|
|
811
|
+
const cacheHitText = cacheHit === null ? null : t("pill.cacheHit", { percent: cacheHit });
|
|
812
|
+
|
|
813
|
+
const segments = [t("pill.tokens", { count: formatTokens(total) })];
|
|
814
|
+
const label = h(
|
|
815
|
+
React.Fragment,
|
|
816
|
+
null,
|
|
817
|
+
segments[0],
|
|
818
|
+
moneyNode !== null
|
|
819
|
+
? h(React.Fragment, null, h("span", { className: "dshuc_sep", "aria-hidden": true }, "·"), moneyNode)
|
|
820
|
+
: null,
|
|
821
|
+
cacheHitText !== null
|
|
822
|
+
? h(
|
|
823
|
+
React.Fragment,
|
|
824
|
+
null,
|
|
825
|
+
h("span", { className: "dshuc_sep", "aria-hidden": true }, "·"),
|
|
826
|
+
cacheHitText,
|
|
827
|
+
)
|
|
828
|
+
: null,
|
|
829
|
+
);
|
|
830
|
+
const text = segments
|
|
831
|
+
.concat(money === null ? [] : [money])
|
|
832
|
+
.concat(cacheHitText === null ? [] : [cacheHitText])
|
|
833
|
+
.join(" · ");
|
|
834
|
+
|
|
835
|
+
const rows = [
|
|
836
|
+
cacheHit !== null ? [t("dialog.cacheHit"), cacheHit + "%"] : null,
|
|
837
|
+
[t("dialog.input"), exactCount(num(usage.uncachedInputTokens))],
|
|
838
|
+
[t("dialog.cacheRead"), exactCount(num(usage.cacheReadTokens))],
|
|
839
|
+
num(usage.cacheWriteTokens) !== 0
|
|
840
|
+
? [t("dialog.cacheWrite"), exactCount(num(usage.cacheWriteTokens))]
|
|
841
|
+
: null,
|
|
842
|
+
[t("dialog.output"), exactCount(output)],
|
|
843
|
+
].filter((row) => row !== null);
|
|
844
|
+
|
|
845
|
+
return h(
|
|
846
|
+
DialogPill,
|
|
847
|
+
{
|
|
848
|
+
stat: "usage",
|
|
849
|
+
icon: h(primitives.IconDatabaseOutlineRegular, {}),
|
|
850
|
+
label,
|
|
851
|
+
text,
|
|
852
|
+
title: t("dialog.title"),
|
|
853
|
+
titleValue: exactCount(total) + (titleMoney === null ? "" : " · " + titleMoney),
|
|
854
|
+
},
|
|
855
|
+
h(
|
|
856
|
+
"dl",
|
|
857
|
+
{ className: "dshuc_details", "data-session-stats-usage": true },
|
|
858
|
+
rows.map(([key, value], index) =>
|
|
859
|
+
h(
|
|
860
|
+
React.Fragment,
|
|
861
|
+
{ key: index },
|
|
862
|
+
h("dt", null, key),
|
|
863
|
+
h("dd", null, value),
|
|
864
|
+
),
|
|
865
|
+
),
|
|
866
|
+
),
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Hidden anchor in the turn tail: rewrites the built-in usage action label
|
|
872
|
+
* from "用量 N tok" to "N tok · ≈¥x.xx" without adding a line of its own.
|
|
873
|
+
*/
|
|
874
|
+
function TurnUsageChip({ turn, t, useChat, useProjection }) {
|
|
875
|
+
const config = useConfig();
|
|
876
|
+
const selection = typeof useProjection === "function" ? useProjection("modelSelection") : undefined;
|
|
877
|
+
const turnNumber = typeof turn === "number" ? turn : turn !== null && typeof turn === "object" ? turn.turn : undefined;
|
|
878
|
+
const useChatSafe = typeof useChat === "function" ? useChat : () => undefined;
|
|
879
|
+
const usage = useChatSafe((snapshot) => {
|
|
880
|
+
if (snapshot === null || typeof snapshot !== "object" || snapshot.nodes === undefined) return undefined;
|
|
881
|
+
const nodes = snapshot.nodes;
|
|
882
|
+
if (typeof nodes.values !== "function") return undefined;
|
|
883
|
+
let found;
|
|
884
|
+
for (const node of nodes.values()) {
|
|
885
|
+
if (node === null || typeof node !== "object" || node.kind !== "turn-tail") continue;
|
|
886
|
+
if (node.data?.turn === turnNumber && node.data?.tokenUsage !== undefined) found = node.data.tokenUsage;
|
|
887
|
+
}
|
|
888
|
+
let summed = null;
|
|
889
|
+
if (Array.isArray(snapshot.legacy?.nodes)) {
|
|
890
|
+
let uncached = 0;
|
|
891
|
+
let cacheRead = 0;
|
|
892
|
+
let cacheWrite = 0;
|
|
893
|
+
let output = 0;
|
|
894
|
+
let total = 0;
|
|
895
|
+
for (const node of snapshot.legacy.nodes) {
|
|
896
|
+
if (node === null || typeof node !== "object" || node.kind !== "assistant" || node.turn !== turnNumber) continue;
|
|
897
|
+
const sample = node.usage;
|
|
898
|
+
if (sample === null || sample === undefined) continue;
|
|
899
|
+
const input = Number(sample.inputTokens) || 0;
|
|
900
|
+
const read = Number(sample.cacheReadTokens) || 0;
|
|
901
|
+
const write = Number(sample.cacheWriteTokens) || 0;
|
|
902
|
+
const out = Number(sample.outputTokens) || 0;
|
|
903
|
+
uncached += input;
|
|
904
|
+
cacheRead += read;
|
|
905
|
+
cacheWrite += write;
|
|
906
|
+
output += out;
|
|
907
|
+
total += Number(sample.totalTokens) || input + read + write + out;
|
|
908
|
+
}
|
|
909
|
+
if (total > 0) {
|
|
910
|
+
summed = {
|
|
911
|
+
uncachedInputTokens: uncached,
|
|
912
|
+
cacheReadTokens: cacheRead,
|
|
913
|
+
cacheWriteTokens: cacheWrite,
|
|
914
|
+
outputTokens: output,
|
|
915
|
+
totalTokens: total,
|
|
916
|
+
estimated: true,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return found ?? summed;
|
|
921
|
+
});
|
|
922
|
+
const anchorRef = React.useRef(null);
|
|
923
|
+
|
|
924
|
+
let text = null;
|
|
925
|
+
if (usage !== undefined && usage !== null) {
|
|
926
|
+
const total = typeof usage.totalTokens === "number" ? usage.totalTokens : totalTokens(usage);
|
|
927
|
+
const route = usage.routes?.[0] ?? selection?.pending ?? selection?.lastUsed ?? null;
|
|
928
|
+
const priced = priceFor(route);
|
|
929
|
+
const usd = priced === null ? null : costOf(usage, priced.rates);
|
|
930
|
+
const money = moneyText(usd, config.currency, config.usdToCny);
|
|
931
|
+
text = (usage.estimated ? "≈" : "") + formatTokens(total) + " tok" + (money === null ? "" : " · " + money);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
React.useEffect(() => {
|
|
935
|
+
const anchor = anchorRef.current;
|
|
936
|
+
if (anchor === null || text === null) return;
|
|
937
|
+
const root = anchor.closest("[data-turn-tail]");
|
|
938
|
+
if (root === null) return;
|
|
939
|
+
let original = null;
|
|
940
|
+
let patched = null;
|
|
941
|
+
const apply = () => {
|
|
942
|
+
const button = root.querySelector('button[aria-haspopup="dialog"]');
|
|
943
|
+
const label = button === null ? null : button.lastElementChild;
|
|
944
|
+
if (label === null) return;
|
|
945
|
+
const textNode = label.firstChild;
|
|
946
|
+
if (original === null) original = textNode !== null && textNode.nodeType === 3 ? textNode.nodeValue : label.textContent;
|
|
947
|
+
// Keep the text node's identity: replacing it would leave React's
|
|
948
|
+
// fiber pointing at a detached node and break a later unmount.
|
|
949
|
+
if (textNode !== null && textNode.nodeType === 3) {
|
|
950
|
+
if (textNode.nodeValue !== text) textNode.nodeValue = text;
|
|
951
|
+
patched = textNode;
|
|
952
|
+
} else if (label.textContent !== text) {
|
|
953
|
+
label.textContent = text;
|
|
954
|
+
patched = label;
|
|
955
|
+
} else {
|
|
956
|
+
patched = label;
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
apply();
|
|
960
|
+
const observer = new MutationObserver(apply);
|
|
961
|
+
observer.observe(root, { subtree: true, childList: true, characterData: true });
|
|
962
|
+
return () => {
|
|
963
|
+
observer.disconnect();
|
|
964
|
+
if (patched === null || original === null) return;
|
|
965
|
+
if (!patched.isConnected) return;
|
|
966
|
+
if (patched.nodeType === 3 && patched.nodeValue === text) patched.nodeValue = original;
|
|
967
|
+
else if (patched.nodeType !== 3 && patched.textContent === text) patched.textContent = original;
|
|
968
|
+
};
|
|
969
|
+
}, [text]);
|
|
970
|
+
|
|
971
|
+
return h("span", { ref: anchorRef, hidden: true, "data-dshuc-anchor": true });
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// -------------------------------------------------------------- config page
|
|
975
|
+
function UsageCostConfigForm({ form, t }) {
|
|
976
|
+
const state = form.state ?? {};
|
|
977
|
+
const value = state.value ?? {};
|
|
978
|
+
const revision = state.revision;
|
|
979
|
+
const [draft, setDraft] = React.useState(() => ({
|
|
980
|
+
currency: sanitizeCurrency(value.currency),
|
|
981
|
+
rate: String(sanitizeRate(value.usdToCny)),
|
|
982
|
+
showBalance: value.showBalance !== false,
|
|
983
|
+
}));
|
|
984
|
+
const [status, setStatus] = React.useState("idle");
|
|
985
|
+
|
|
986
|
+
React.useEffect(() => {
|
|
987
|
+
setDraft({
|
|
988
|
+
currency: sanitizeCurrency(value.currency),
|
|
989
|
+
rate: String(sanitizeRate(value.usdToCny)),
|
|
990
|
+
showBalance: value.showBalance !== false,
|
|
991
|
+
});
|
|
992
|
+
setStatus("idle");
|
|
993
|
+
// Reset the draft only when the Host revision moves.
|
|
994
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
995
|
+
}, [revision]);
|
|
996
|
+
|
|
997
|
+
const readOnly = state.writable === false;
|
|
998
|
+
const busy = status === "saving";
|
|
999
|
+
const canToggleBalance = Object.hasOwn(value, "showBalance");
|
|
1000
|
+
|
|
1001
|
+
const save = async () => {
|
|
1002
|
+
const rate = Number(draft.rate.trim());
|
|
1003
|
+
if (!Number.isFinite(rate) || rate < 1 || rate > 100) {
|
|
1004
|
+
setStatus("invalid");
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const ops = [];
|
|
1008
|
+
if (draft.currency !== sanitizeCurrency(value.currency)) {
|
|
1009
|
+
ops.push({ op: "set", path: ["currency"], value: draft.currency });
|
|
1010
|
+
}
|
|
1011
|
+
if (rate !== sanitizeRate(value.usdToCny)) {
|
|
1012
|
+
ops.push({ op: "set", path: ["usdToCny"], value: rate });
|
|
1013
|
+
}
|
|
1014
|
+
if (canToggleBalance && draft.showBalance !== (value.showBalance !== false)) {
|
|
1015
|
+
ops.push({ op: "set", path: ["showBalance"], value: draft.showBalance });
|
|
1016
|
+
}
|
|
1017
|
+
if (ops.length === 0) {
|
|
1018
|
+
setStatus("saved");
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
setStatus("saving");
|
|
1022
|
+
try {
|
|
1023
|
+
const accepted = await form.mutate(ops, revision);
|
|
1024
|
+
setStatus(accepted ? "saved" : "failed");
|
|
1025
|
+
} catch {
|
|
1026
|
+
setStatus("failed");
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
const reset = async () => {
|
|
1031
|
+
setStatus("saving");
|
|
1032
|
+
const ops = [
|
|
1033
|
+
{ op: "unset", path: ["currency"] },
|
|
1034
|
+
{ op: "unset", path: ["usdToCny"] },
|
|
1035
|
+
];
|
|
1036
|
+
if (canToggleBalance) ops.push({ op: "unset", path: ["showBalance"] });
|
|
1037
|
+
try {
|
|
1038
|
+
const accepted = await form.mutate(ops, revision);
|
|
1039
|
+
setStatus(accepted ? "saved" : "failed");
|
|
1040
|
+
} catch {
|
|
1041
|
+
setStatus("failed");
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
const message =
|
|
1046
|
+
status === "saved"
|
|
1047
|
+
? t("config.saved")
|
|
1048
|
+
: status === "failed"
|
|
1049
|
+
? t("config.failed")
|
|
1050
|
+
: status === "invalid"
|
|
1051
|
+
? t("config.invalidRate")
|
|
1052
|
+
: readOnly
|
|
1053
|
+
? t("config.readOnly")
|
|
1054
|
+
: "";
|
|
1055
|
+
|
|
1056
|
+
return h(
|
|
1057
|
+
"div",
|
|
1058
|
+
{ className: "dshuc_cfg" },
|
|
1059
|
+
h("div", { className: "dshuc_cfgTitle" }, t("config.title")),
|
|
1060
|
+
canToggleBalance
|
|
1061
|
+
? h(
|
|
1062
|
+
"div",
|
|
1063
|
+
{ className: "dshuc_field" },
|
|
1064
|
+
h(
|
|
1065
|
+
"label",
|
|
1066
|
+
{ className: "dshuc_toggleRow" },
|
|
1067
|
+
h("span", { className: "dshuc_fieldLabel" }, t("config.showBalance")),
|
|
1068
|
+
h(primitives.Switch, {
|
|
1069
|
+
checked: draft.showBalance,
|
|
1070
|
+
label: t("config.showBalance"),
|
|
1071
|
+
disabled: readOnly || busy,
|
|
1072
|
+
onChange: (checked) => setDraft({ ...draft, showBalance: checked }),
|
|
1073
|
+
}),
|
|
1074
|
+
),
|
|
1075
|
+
h("span", { className: "dshuc_hint" }, t("config.showBalanceHint")),
|
|
1076
|
+
)
|
|
1077
|
+
: null,
|
|
1078
|
+
h(
|
|
1079
|
+
"label",
|
|
1080
|
+
{ className: "dshuc_field" },
|
|
1081
|
+
h("span", { className: "dshuc_fieldLabel" }, t("config.currency")),
|
|
1082
|
+
h(
|
|
1083
|
+
"select",
|
|
1084
|
+
{
|
|
1085
|
+
className: "dshuc_input",
|
|
1086
|
+
value: draft.currency,
|
|
1087
|
+
disabled: readOnly || busy,
|
|
1088
|
+
onChange: (event) => setDraft({ ...draft, currency: event.target.value }),
|
|
1089
|
+
},
|
|
1090
|
+
h("option", { value: "CNY" }, t("currency.cny")),
|
|
1091
|
+
h("option", { value: "USD" }, t("currency.usd")),
|
|
1092
|
+
h("option", { value: "BOTH" }, t("currency.both")),
|
|
1093
|
+
),
|
|
1094
|
+
),
|
|
1095
|
+
h(
|
|
1096
|
+
"label",
|
|
1097
|
+
{ className: "dshuc_field" },
|
|
1098
|
+
h("span", { className: "dshuc_fieldLabel" }, t("config.rate")),
|
|
1099
|
+
h("input", {
|
|
1100
|
+
className: "dshuc_input",
|
|
1101
|
+
type: "number",
|
|
1102
|
+
min: 1,
|
|
1103
|
+
max: 100,
|
|
1104
|
+
step: 0.01,
|
|
1105
|
+
value: draft.rate,
|
|
1106
|
+
disabled: readOnly || busy,
|
|
1107
|
+
onChange: (event) => setDraft({ ...draft, rate: event.target.value }),
|
|
1108
|
+
}),
|
|
1109
|
+
h("span", { className: "dshuc_hint" }, t("config.rateHint")),
|
|
1110
|
+
),
|
|
1111
|
+
h(
|
|
1112
|
+
"div",
|
|
1113
|
+
{ className: "dshuc_field" },
|
|
1114
|
+
h("span", { className: "dshuc_fieldLabel" }, t("config.ratesTitle")),
|
|
1115
|
+
h(
|
|
1116
|
+
"table",
|
|
1117
|
+
{ className: "dshuc_table" },
|
|
1118
|
+
h(
|
|
1119
|
+
"thead",
|
|
1120
|
+
null,
|
|
1121
|
+
h(
|
|
1122
|
+
"tr",
|
|
1123
|
+
null,
|
|
1124
|
+
h("th", null, t("config.model")),
|
|
1125
|
+
h("th", null, t("dialog.input")),
|
|
1126
|
+
h("th", null, t("dialog.cacheRead")),
|
|
1127
|
+
h("th", null, t("dialog.cacheWrite")),
|
|
1128
|
+
h("th", null, t("dialog.output")),
|
|
1129
|
+
),
|
|
1130
|
+
),
|
|
1131
|
+
h(
|
|
1132
|
+
"tbody",
|
|
1133
|
+
null,
|
|
1134
|
+
Object.entries(MODEL_PRICES).map(([model, rates]) =>
|
|
1135
|
+
h(
|
|
1136
|
+
"tr",
|
|
1137
|
+
{ key: model },
|
|
1138
|
+
h("td", null, model),
|
|
1139
|
+
h("td", null, "$" + rates.input),
|
|
1140
|
+
h("td", null, "$" + rates.cacheRead),
|
|
1141
|
+
h("td", null, "$" + rates.cacheWrite),
|
|
1142
|
+
h("td", null, "$" + rates.output),
|
|
1143
|
+
),
|
|
1144
|
+
),
|
|
1145
|
+
),
|
|
1146
|
+
),
|
|
1147
|
+
h("span", { className: "dshuc_hint" }, t("config.ratesNote")),
|
|
1148
|
+
),
|
|
1149
|
+
h(
|
|
1150
|
+
"div",
|
|
1151
|
+
{ className: "dshuc_actions" },
|
|
1152
|
+
h(
|
|
1153
|
+
primitives.Button,
|
|
1154
|
+
{ variant: "primary", size: "sm", disabled: readOnly || busy, onClick: () => void save() },
|
|
1155
|
+
busy ? t("config.saving") : t("config.save"),
|
|
1156
|
+
),
|
|
1157
|
+
h(primitives.Button, { variant: "outline", size: "sm", disabled: readOnly || busy, onClick: () => void reset() }, t("config.reset")),
|
|
1158
|
+
),
|
|
1159
|
+
message === "" ? null : h("div", { className: "dshuc_status", "data-tone": status === "failed" || status === "invalid" ? "error" : undefined }, message),
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function UsageCostConfigPage(props) {
|
|
1164
|
+
const { view, form, t } = props;
|
|
1165
|
+
if (view === "summary") return t("config.summary");
|
|
1166
|
+
if (form === undefined || form === null) {
|
|
1167
|
+
return h("p", { className: "dshuc_cfgNote" }, t("config.unavailable"));
|
|
1168
|
+
}
|
|
1169
|
+
return h(UsageCostConfigForm, { form, t });
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// -------------------------------------------------------------------- apply
|
|
1173
|
+
function apply(ctx) {
|
|
1174
|
+
ctx.effect(
|
|
1175
|
+
() =>
|
|
1176
|
+
ctx.locale.register(NS, {
|
|
1177
|
+
zh,
|
|
1178
|
+
en,
|
|
1179
|
+
ja: en,
|
|
1180
|
+
es: en,
|
|
1181
|
+
fr: en,
|
|
1182
|
+
de: en,
|
|
1183
|
+
ru: en,
|
|
1184
|
+
pt: en,
|
|
1185
|
+
}),
|
|
1186
|
+
"dsh-usage-cost: dictionaries",
|
|
1187
|
+
);
|
|
1188
|
+
injectStyles();
|
|
1189
|
+
attachConfig(ctx);
|
|
1190
|
+
attachBalance(ctx);
|
|
1191
|
+
ctx.slots.inject("conversation.composer.dock", () =>
|
|
1192
|
+
ctx.slots.register(
|
|
1193
|
+
{
|
|
1194
|
+
name: "conversation.composer.dock",
|
|
1195
|
+
id: "usage",
|
|
1196
|
+
order: 1,
|
|
1197
|
+
priority: -1,
|
|
1198
|
+
locale: NS,
|
|
1199
|
+
},
|
|
1200
|
+
SessionUsageCostPill,
|
|
1201
|
+
),
|
|
1202
|
+
);
|
|
1203
|
+
ctx.slots.inject("conversation.chat.turnTail", () =>
|
|
1204
|
+
ctx.slots.register(
|
|
1205
|
+
{
|
|
1206
|
+
name: "conversation.chat.turnTail",
|
|
1207
|
+
id: "turn-usage-cost",
|
|
1208
|
+
order: 40,
|
|
1209
|
+
locale: NS,
|
|
1210
|
+
},
|
|
1211
|
+
TurnUsageChip,
|
|
1212
|
+
),
|
|
1213
|
+
);
|
|
1214
|
+
ctx.slots.inject("plugins.row.config", () =>
|
|
1215
|
+
ctx.slots.register(
|
|
1216
|
+
{
|
|
1217
|
+
name: "plugins.row.config",
|
|
1218
|
+
key: CONFIG_KEY,
|
|
1219
|
+
locale: NS,
|
|
1220
|
+
},
|
|
1221
|
+
UsageCostConfigPage,
|
|
1222
|
+
),
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
exports.name = "dsh-usage-cost";
|
|
1227
|
+
exports.inject = ["slots", "locale"];
|
|
1228
|
+
exports.apply = apply;
|
|
1229
|
+
/** Test-only internals (loaded through the stub module factory). */
|
|
1230
|
+
exports.__test = {
|
|
1231
|
+
MODEL_PRICES,
|
|
1232
|
+
priceFor,
|
|
1233
|
+
costOf,
|
|
1234
|
+
cacheHitPercent,
|
|
1235
|
+
formatTokens,
|
|
1236
|
+
amount,
|
|
1237
|
+
moneyText,
|
|
1238
|
+
billedInput,
|
|
1239
|
+
totalTokens,
|
|
1240
|
+
createConfigStore,
|
|
1241
|
+
sanitizeCurrency,
|
|
1242
|
+
sanitizeRate,
|
|
1243
|
+
formatMoney,
|
|
1244
|
+
compactMoney,
|
|
1245
|
+
walletListText,
|
|
1246
|
+
createBalanceStore,
|
|
1247
|
+
};
|
|
1248
|
+
return module.exports;
|
|
1249
|
+
},
|
|
1250
|
+
});
|