@guojinpeng/dsh-cost-meter 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js ADDED
@@ -0,0 +1,866 @@
1
+ // src/pricing.js
2
+ var RATE_KEYS = ["input", "output", "cacheRead", "cacheWrite"];
3
+ var zeroRate = () => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
4
+ function normalizeRate(entry) {
5
+ if (!entry || typeof entry !== "object") return null;
6
+ if (!["input", "output"].every(
7
+ (key) => typeof entry[key] === "number" && Number.isFinite(entry[key]) && entry[key] >= 0
8
+ ))
9
+ return null;
10
+ return Object.fromEntries(
11
+ RATE_KEYS.map((key) => [key, Number.isFinite(entry[key]) && entry[key] >= 0 ? entry[key] : 0])
12
+ );
13
+ }
14
+ function parseRouteKey(key) {
15
+ const slash = key.indexOf("/");
16
+ return slash > 0 && slash < key.length - 1 ? { provider: key.slice(0, slash), model: key.slice(slash + 1) } : null;
17
+ }
18
+ function tokensOf(buckets) {
19
+ return buckets.uncachedInputTokens + buckets.outputTokens + buckets.cacheReadTokens + buckets.cacheWriteTokens;
20
+ }
21
+ function wireTokens(buckets) {
22
+ return {
23
+ input: buckets.uncachedInputTokens,
24
+ output: buckets.outputTokens,
25
+ read: buckets.cacheReadTokens,
26
+ write: buckets.cacheWriteTokens
27
+ };
28
+ }
29
+ function priceRoutes(byRoute, pricing) {
30
+ let micros = 0, unpricedTokens = 0;
31
+ const bucketMicros = { input: 0, output: 0, read: 0, write: 0 };
32
+ const routes = [];
33
+ for (const [key, buckets] of Object.entries(byRoute)) {
34
+ const rate = normalizeRate(pricing[key]);
35
+ const totalTokens = tokensOf(buckets);
36
+ let amount = 0;
37
+ if (rate === null) unpricedTokens += totalTokens;
38
+ else {
39
+ const parts = {
40
+ input: buckets.uncachedInputTokens * rate.input,
41
+ output: buckets.outputTokens * rate.output,
42
+ read: buckets.cacheReadTokens * rate.cacheRead,
43
+ write: buckets.cacheWriteTokens * rate.cacheWrite
44
+ };
45
+ amount = Math.round(Object.values(parts).reduce((sum, value) => sum + value, 0));
46
+ micros += amount;
47
+ for (const field of Object.keys(parts)) bucketMicros[field] += parts[field];
48
+ }
49
+ routes.push({ route: key, micros: amount, totalTokens, priced: rate !== null });
50
+ }
51
+ for (const key of Object.keys(bucketMicros)) bucketMicros[key] = Math.round(bucketMicros[key]);
52
+ return { micros, unpricedTokens, bucketMicros, routes };
53
+ }
54
+ function buildCostView(accounting, config) {
55
+ const { micros: totalMicros, unpricedTokens, bucketMicros, routes } = priceRoutes(accounting.byRoute, config.pricing);
56
+ routes.sort((a, b) => b.micros - a.micros || b.totalTokens - a.totalTokens);
57
+ const turns = {};
58
+ for (const [key, entry] of Object.entries(accounting.turns)) {
59
+ const priced = priceRoutes(entry.byRoute, config.pricing);
60
+ turns[key] = {
61
+ micros: priced.micros,
62
+ totalTokens: entry.totalTokens,
63
+ route: Object.keys(entry.byRoute).join(", "),
64
+ tokens: wireTokens(entry.tokens),
65
+ bucketMicros: priced.bucketMicros,
66
+ priced: priced.unpricedTokens === 0,
67
+ recovered: entry.recovered
68
+ };
69
+ }
70
+ const prompt = tokensOf(accounting.totals) - accounting.totals.outputTokens;
71
+ return {
72
+ currency: config.currency,
73
+ cnyPerUsd: config.cnyPerUsd,
74
+ totalMicros,
75
+ totalTokens: tokensOf(accounting.totals),
76
+ tokens: wireTokens(accounting.totals),
77
+ bucketMicros,
78
+ cacheHitPct: prompt === 0 ? null : Math.round(accounting.totals.cacheReadTokens / prompt * 1e3) / 10,
79
+ skippedTurns: accounting.skippedTurns,
80
+ unpricedTokens,
81
+ showDock: config.showDockChip !== false,
82
+ showTurn: config.showTurnChip !== false,
83
+ routes: routes.slice(0, 8),
84
+ turns
85
+ };
86
+ }
87
+
88
+ // src/identity.js
89
+ var PACKAGE_ID = "@guojinpeng/dsh-cost-meter";
90
+ var PLUGIN_ID = "guojinpeng/dsh-cost-meter";
91
+ var HTTP_BASE = "/guojinpeng/dsh-cost-meter";
92
+ var PROJECTION_KEY = "guojinpengCostUsage";
93
+ var LOCALE_NAMESPACE = "guojinpengDshCostMeter";
94
+
95
+ // src/client-store.js
96
+ function createConfigStore(fetcher = (...args) => fetch(...args)) {
97
+ let snapshot = null, sequence = 0, loading = null, loadStatus = "idle";
98
+ const listeners = /* @__PURE__ */ new Set();
99
+ const notify = () => {
100
+ for (const listener of listeners) listener();
101
+ };
102
+ const publish = (value) => {
103
+ snapshot = value;
104
+ notify();
105
+ return value;
106
+ };
107
+ async function request(path, body) {
108
+ const response = await fetcher(
109
+ `${HTTP_BASE}/${path}`,
110
+ body === void 0 ? void 0 : {
111
+ method: "POST",
112
+ headers: { "content-type": "application/json" },
113
+ body: JSON.stringify(body)
114
+ }
115
+ );
116
+ const payload = await response.json();
117
+ if (!response.ok) throw new Error(payload.error ?? `HTTP ${response.status}`);
118
+ return payload;
119
+ }
120
+ return {
121
+ getSnapshot: () => snapshot,
122
+ getLoadStatus: () => loadStatus,
123
+ subscribe: (listener) => {
124
+ listeners.add(listener);
125
+ return () => listeners.delete(listener);
126
+ },
127
+ load: () => {
128
+ if (loading) return loading;
129
+ const ticket = ++sequence;
130
+ loadStatus = "loading";
131
+ loading = request("config").then((value) => ticket === sequence ? publish(value) : snapshot ?? value).catch((error) => {
132
+ loadStatus = "error";
133
+ throw error;
134
+ }).finally(() => {
135
+ loading = null;
136
+ if (loadStatus !== "error") loadStatus = "ready";
137
+ notify();
138
+ });
139
+ notify();
140
+ return loading;
141
+ },
142
+ save: async (config) => {
143
+ const ticket = ++sequence;
144
+ const value = await request("config", { config, revision: snapshot == null ? void 0 : snapshot.revision });
145
+ return ticket === sequence ? publish(value) : snapshot;
146
+ },
147
+ sync: async () => {
148
+ const ticket = ++sequence;
149
+ const value = await request("pricing/sync", { source: "models.dev", revision: snapshot == null ? void 0 : snapshot.revision });
150
+ return ticket === sequence ? publish(value) : snapshot;
151
+ }
152
+ };
153
+ }
154
+ function buildDraft(payload) {
155
+ const config = payload.config;
156
+ return {
157
+ currency: config.currency,
158
+ cnyPerUsd: config.cnyPerUsd,
159
+ showDockChip: config.showDockChip,
160
+ showTurnChip: config.showTurnChip,
161
+ rows: payload.models.map((key) => ({ key, ...config.pricing[key] ?? zeroRate() }))
162
+ };
163
+ }
164
+ function configOf(draft) {
165
+ const cnyPerUsd = Number(draft.cnyPerUsd);
166
+ if (!Number.isFinite(cnyPerUsd) || cnyPerUsd <= 0) throw new Error("Invalid USD/CNY exchange rate");
167
+ const pricing = {};
168
+ for (const row of draft.rows) {
169
+ if (RATE_KEYS.some(
170
+ (key) => String(row[key]).trim() === "" || !Number.isFinite(Number(row[key])) || Number(row[key]) < 0
171
+ )) {
172
+ throw new Error(`Invalid rate: ${row.key}`);
173
+ }
174
+ pricing[row.key] = Object.fromEntries(RATE_KEYS.map((key) => [key, Number(row[key])]));
175
+ }
176
+ return {
177
+ currency: draft.currency,
178
+ cnyPerUsd,
179
+ showDockChip: draft.showDockChip,
180
+ showTurnChip: draft.showTurnChip,
181
+ pricing
182
+ };
183
+ }
184
+
185
+ // src/client.js
186
+ window.__ModuleLoader__.load({
187
+ id: PACKAGE_ID,
188
+ factory: (e) => {
189
+ var t = { exports: {} }, n = t.exports;
190
+ Object.defineProperty(n, Symbol.toStringTag, { value: `Module` });
191
+ const react = e(`react`);
192
+ const reactDom = e(`react-dom`);
193
+ const h = react.createElement;
194
+ const store = createConfigStore();
195
+ function useCostView(props) {
196
+ const raw = props.useProjection(PROJECTION_KEY);
197
+ const snapshot = react.useSyncExternalStore(store.subscribe, store.getSnapshot);
198
+ const loadStatus = react.useSyncExternalStore(store.subscribe, store.getLoadStatus);
199
+ const missing = !snapshot || (raw == null ? void 0 : raw.accounting) && Object.keys(raw.accounting.byRoute).some((key) => key && !snapshot.config.pricing[key]);
200
+ react.useEffect(() => {
201
+ if (!snapshot || (raw == null ? void 0 : raw.accounting) && Object.keys(raw.accounting.byRoute).some((key) => key && !snapshot.config.pricing[key]))
202
+ store.load().catch(() => {
203
+ });
204
+ }, [raw]);
205
+ return react.useMemo(() => {
206
+ const view = (raw == null ? void 0 : raw.accounting) && snapshot ? buildCostView(raw.accounting, snapshot.config) : raw;
207
+ return missing && loadStatus !== "error" ? { ...view, initializing: true } : view;
208
+ }, [raw, snapshot, missing, loadStatus]);
209
+ }
210
+ const NS = LOCALE_NAMESPACE;
211
+ const CSS_TAG = `${PLUGIN_ID}/cost-chip.module.css`;
212
+ const CSS = `:root{--guojinpeng-cost-money:var(--dsw-alias-label-secondary)}
213
+ .gdcc_dockRoot{box-sizing:border-box;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;justify-content:center;gap:12px}
214
+ .gdcc_dockPill{box-sizing:border-box;corner-shape:round;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;border-radius:999px;align-items:center;gap:6px;padding:1px 8px;display:inline-flex;cursor:pointer}
215
+ .gdcc_dockPill svg{flex:none;width:14px;height:14px}
216
+ .gdcc_dockPill:hover,.gdcc_dockPill[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
217
+ .gdcc_dockLabel{text-overflow:ellipsis;min-width:0;overflow:hidden}
218
+ .gdcc_money{color:var(--guojinpeng-cost-money);font-variant-numeric:tabular-nums}
219
+ .gdcc_turnRoot{min-width:0;display:inline-flex;order:1}
220
+ .gdcc_turnTrigger{min-width:0;height:calc(28px + var(--dsh-content-font-delta,0px));border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-tertiary);font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);font-variant-numeric:tabular-nums;line-height:calc(24px + var(--dsh-content-font-delta,0px));white-space:nowrap;cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;padding:6px 8px;display:inline-flex}
221
+ .gdcc_turnLabel{text-overflow:ellipsis;min-width:0;overflow:hidden}
222
+ .gdcc_turnTrigger svg{width:calc(15px + var(--dsh-content-font-delta,0px));height:calc(15px + var(--dsh-content-font-delta,0px));flex:none}
223
+ .gdcc_turnTrigger:hover,.gdcc_turnTrigger[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary)}
224
+ .gdcc_initializing{pointer-events:none}
225
+ @media (width<=480px){.gdcc_turnTrigger{width:calc(28px + var(--dsh-content-font-delta,0px));justify-content:center;padding:6px}.gdcc_turnTrigger .gdcc_turnLabel{display:none}}
226
+ .gdcc_turnTrigger.gdcc_initializing{width:auto}
227
+ .gdcc_panel{z-index:1100;box-sizing:border-box;border-radius:var(--dsw-radius-lg);background:var(--dsw-specific-menu);width:max-content;min-width:min(300px,100vw - 24px);max-width:min(440px,100vw - 24px);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;border:0;padding:16px;font-size:12px;line-height:18px;position:fixed}
228
+ .gdcc_title{color:var(--dsw-alias-label-primary);justify-content:space-between;gap:16px;margin-bottom:8px;font-weight:500;display:flex}
229
+ .gdcc_titleRule{border-top:.5px solid var(--dsw-alias-border-l2);margin-bottom:10px}
230
+ .gdcc_titleValue{font-variant-numeric:tabular-nums}
231
+ .gdcc_titleLabel{align-items:center;gap:6px;min-width:0;display:inline-flex}
232
+ .gdcc_titleLabel svg{flex:none;width:14px;height:14px}
233
+ .gdcc_details{color:var(--dsw-alias-label-tertiary);grid-template-columns:minmax(76px,auto) minmax(0,1fr);gap:6px 16px;margin:0;display:grid}
234
+ .gdcc_details dt,.gdcc_details dd{min-width:0;margin:0}
235
+ .gdcc_details dd{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;text-align:right}
236
+ .gdcc_details .gdcc_route{overflow-wrap:anywhere}
237
+ .gdcc_sub{color:var(--dsw-alias-label-tertiary);margin:12px 0 6px;font-weight:500}
238
+ .gdcc_note{color:var(--dsw-alias-label-tertiary);margin-top:10px}
239
+ .gdcc_form{border:0;padding:0;margin:0;min-width:0;display:grid;gap:12px;max-width:760px}
240
+ .gdcc_fields{display:grid;gap:8px}
241
+ .gdcc_field{display:grid;grid-template-columns:minmax(96px,auto) minmax(0,1fr);align-items:center;gap:12px}
242
+ .gdcc_fieldLabel{color:var(--dsw-alias-label-secondary)}
243
+ .gdcc_input{box-sizing:border-box;width:100%;min-width:0;font:inherit;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-sm);padding:4px 8px}
244
+ .gdcc_input:disabled{color:var(--dsw-alias-label-tertiary);cursor:not-allowed}
245
+ .gdcc_input[type=checkbox]{width:auto;justify-self:start}
246
+ .gdcc_rows{display:grid;gap:6px;overflow-x:auto}.gdcc_model{overflow-wrap:anywhere}.gdcc_row{min-width:620px}.gdcc_actions{flex-wrap:wrap}.gdcc_ok,.gdcc_error{overflow-wrap:anywhere;min-width:0}
247
+ .gdcc_row{display:grid;grid-template-columns:minmax(0,1.6fr) repeat(4,minmax(0,1fr)) auto;align-items:center;gap:6px}
248
+ .gdcc_rowHead{color:var(--dsw-alias-label-tertiary);font-size:11px}
249
+ .gdcc_provider{min-width:620px;margin-top:8px;padding:8px 0 5px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);font-weight:500;overflow-wrap:anywhere}
250
+ .gdcc_model{padding-left:12px}
251
+ .gdcc_btn{font:inherit;color:var(--dsw-alias-label-secondary);background:0 0;border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-sm);padding:3px 10px;cursor:pointer}
252
+ .gdcc_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
253
+ .gdcc_btn:disabled{color:var(--dsw-alias-label-tertiary);cursor:not-allowed}
254
+ .gdcc_btnPrimary{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}
255
+ .gdcc_actions{display:flex;align-items:center;gap:8px;justify-content:flex-end}
256
+ .gdcc_hint{color:var(--dsw-alias-label-tertiary)}
257
+ .gdcc_error{color:var(--dsw-alias-label-error,var(--dsw-alias-label-primary))}
258
+ .gdcc_ok{color:var(--dsw-alias-label-tertiary)}`;
259
+ if (typeof document !== `undefined` && document.querySelector(`style[data-plugin-css=` + JSON.stringify(CSS_TAG) + `]`) === null) {
260
+ const tag = document.createElement(`style`);
261
+ tag.dataset.plugin = PACKAGE_ID;
262
+ tag.dataset.pluginCss = CSS_TAG;
263
+ tag.textContent = CSS;
264
+ document.head.appendChild(tag);
265
+ }
266
+ function formatExactTokens(value) {
267
+ if (typeof value !== `number` || !Number.isFinite(value)) return `0`;
268
+ const sign = value < 0 ? `-` : ``;
269
+ const digits = String(Math.abs(Math.round(value)));
270
+ const groups = [];
271
+ for (let end = digits.length; end > 0; end -= 3) groups.unshift(digits.slice(Math.max(0, end - 3), end));
272
+ return sign + groups.join(`,`);
273
+ }
274
+ function toDisplay(micros, currency, cnyPerUsd) {
275
+ const usd = micros / 1e6;
276
+ return currency === `CNY` ? usd * (cnyPerUsd || 7.2) : usd;
277
+ }
278
+ function symbolOf(currency) {
279
+ if (currency === `CNY`) return `\xA5`;
280
+ if (currency === `USD`) return `$`;
281
+ return currency ? currency + ` ` : ``;
282
+ }
283
+ function formatMoney(micros, currency, cnyPerUsd) {
284
+ if (micros === null || micros === void 0) return `\u2014`;
285
+ const value = toDisplay(micros, currency, cnyPerUsd);
286
+ const symbol = symbolOf(currency);
287
+ if (value === 0) return symbol + `0.00`;
288
+ if (Math.abs(value) < 0.01) return symbol + value.toFixed(4);
289
+ if (Math.abs(value) < 1) return symbol + value.toFixed(3);
290
+ return symbol + value.toFixed(2);
291
+ }
292
+ function formatChipMoney(micros, currency, cnyPerUsd) {
293
+ if (micros === null || micros === void 0) return `\u2014`;
294
+ const value = toDisplay(micros, currency, cnyPerUsd);
295
+ const symbol = symbolOf(currency);
296
+ if (value === 0) return symbol + `0.00`;
297
+ if (Math.abs(value) < 0.01) return symbol + `<0.01`;
298
+ return symbol + value.toFixed(2);
299
+ }
300
+ function overlayTopMargin(min) {
301
+ const root = document.documentElement;
302
+ const clearance = Number.parseFloat(getComputedStyle(root).getPropertyValue(`--dsh-frame-top-clearance`));
303
+ if (Number.isNaN(clearance)) return min;
304
+ return Math.max(min, (root.hasAttribute(`data-fullscreen`) ? 0 : clearance) + 20);
305
+ }
306
+ function useAnchoredPosition(options) {
307
+ const { open, anchorRef, panelRef, side = `bottom`, align = `start`, gap, margin } = options;
308
+ const [position, setPosition] = react.useState(null);
309
+ react.useLayoutEffect(() => {
310
+ if (!open) {
311
+ setPosition(null);
312
+ return void 0;
313
+ }
314
+ const place = () => {
315
+ var _a;
316
+ const rect = (_a = anchorRef.current) == null ? void 0 : _a.getBoundingClientRect();
317
+ if (rect === void 0) return;
318
+ const panel3 = panelRef.current;
319
+ const width = (panel3 == null ? void 0 : panel3.offsetWidth) ?? 0;
320
+ const height = (panel3 == null ? void 0 : panel3.offsetHeight) ?? 0;
321
+ let left = align === `end` ? rect.right - width : rect.left;
322
+ let top = side === `top` ? rect.top - gap - height : rect.bottom + gap;
323
+ if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin);
324
+ if (height > 0) top = Math.min(Math.max(top, overlayTopMargin(margin)), window.innerHeight - height - margin);
325
+ setPosition({ left, top });
326
+ };
327
+ place();
328
+ window.addEventListener(`scroll`, place, true);
329
+ window.addEventListener(`resize`, place);
330
+ const panel2 = panelRef.current;
331
+ let observer = null;
332
+ if (typeof ResizeObserver !== `undefined` && panel2 !== null) {
333
+ observer = new ResizeObserver(place);
334
+ observer.observe(panel2);
335
+ }
336
+ return () => {
337
+ observer == null ? void 0 : observer.disconnect();
338
+ window.removeEventListener(`scroll`, place, true);
339
+ window.removeEventListener(`resize`, place);
340
+ };
341
+ }, [open, anchorRef, panelRef, side, align, gap, margin]);
342
+ return position;
343
+ }
344
+ function useDismissOnOutsidePointer(root, open, setOpen, portal) {
345
+ react.useEffect(() => {
346
+ if (!open) return void 0;
347
+ const closeOutside = (event) => {
348
+ var _a, _b;
349
+ if (event.target instanceof Node && ((_a = root.current) == null ? void 0 : _a.contains(event.target)) !== true && ((_b = portal == null ? void 0 : portal.current) == null ? void 0 : _b.contains(event.target)) !== true) {
350
+ setOpen(false);
351
+ }
352
+ };
353
+ document.addEventListener(`pointerdown`, closeOutside);
354
+ return () => {
355
+ document.removeEventListener(`pointerdown`, closeOutside);
356
+ };
357
+ }, [root, open, setOpen, portal]);
358
+ }
359
+ function IconGaugeOutlineRegular(props) {
360
+ const { size = 16, className } = props ?? {};
361
+ return h(
362
+ `svg`,
363
+ {
364
+ width: size,
365
+ height: size,
366
+ className,
367
+ viewBox: `0 0 16 16`,
368
+ fill: `none`,
369
+ xmlns: `http://www.w3.org/2000/svg`,
370
+ "aria-hidden": `true`,
371
+ strokeWidth: 1
372
+ },
373
+ h(`path`, {
374
+ d: `M3.4041 13.096C2.49514 12.187 1.87614 11.0288 1.62537 9.76798C1.37459 8.50716 1.50331 7.20028 1.99525 6.01261C2.48719 4.82494 3.32025 3.80981 4.3891 3.09557C5.45795 2.38134 6.71458 2.00008 8.0001 2C9.28563 2.00008 10.5423 2.38134 11.6111 3.09557C12.68 3.80981 13.513 4.82494 14.005 6.01261C14.4969 7.20028 14.6256 8.50716 14.3748 9.76798C14.1241 11.0288 13.5051 12.187 12.5961 13.096`,
375
+ stroke: `currentColor`
376
+ }),
377
+ h(`path`, { d: `M8 8.49994L11.6114 4.88855`, stroke: `currentColor` }),
378
+ h(`path`, {
379
+ d: `M8 9.75C8.69036 9.75 9.25 9.19036 9.25 8.5C9.25 7.80964 8.69036 7.25 8 7.25C7.30964 7.25 6.75 7.80964 6.75 8.5C6.75 9.19036 7.30964 9.75 8 9.75Z`,
380
+ fill: `currentColor`
381
+ })
382
+ );
383
+ }
384
+ const PANEL_MARGIN = 12;
385
+ const PANEL_GAP = 8;
386
+ const MEASURE_STYLE = { visibility: `hidden`, left: 0, top: 0 };
387
+ function useCostDialog() {
388
+ const [open, setOpen] = react.useState(false);
389
+ const rootRef = react.useRef(null);
390
+ const panelRef = react.useRef(null);
391
+ const pos = useAnchoredPosition({
392
+ open,
393
+ anchorRef: rootRef,
394
+ panelRef,
395
+ side: `top`,
396
+ gap: PANEL_GAP,
397
+ margin: PANEL_MARGIN
398
+ });
399
+ useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef);
400
+ react.useEffect(() => {
401
+ if (!open) return void 0;
402
+ const onKeyDown = (event) => {
403
+ if (event.key === `Escape`) setOpen(false);
404
+ };
405
+ document.addEventListener(`keydown`, onKeyDown);
406
+ return () => document.removeEventListener(`keydown`, onKeyDown);
407
+ }, [open]);
408
+ return { open, setOpen, rootRef, panelRef, pos };
409
+ }
410
+ function row(label, value, key) {
411
+ if (value === null || value === void 0 || value === false) return null;
412
+ return [h(`dt`, { key: key + `-k` }, label), h(`dd`, { key: key + `-v` }, value)];
413
+ }
414
+ function details(items, key) {
415
+ const children = [];
416
+ for (const item of items) {
417
+ const cells = row(item[0], item[1], `r${children.length}`);
418
+ if (cells !== null) children.push(cells[0], cells[1]);
419
+ }
420
+ return h(`dl`, { className: `gdcc_details`, key }, children);
421
+ }
422
+ function panel(props) {
423
+ return reactDom.createPortal(
424
+ h(`div`, {
425
+ ref: props.panelRef,
426
+ className: `gdcc_panel`,
427
+ role: `dialog`,
428
+ "aria-label": props.title,
429
+ style: props.pos ?? MEASURE_STYLE,
430
+ children: [
431
+ h(`div`, { className: `gdcc_title`, key: `title` }, [
432
+ h(`span`, { className: `gdcc_titleLabel`, key: `l` }, [
433
+ h(props.icon, { key: `i` }),
434
+ h(`span`, { key: `t` }, props.title)
435
+ ]),
436
+ h(`span`, { className: `gdcc_titleValue`, key: `v` }, props.value)
437
+ ]),
438
+ h(`div`, { className: `gdcc_titleRule`, key: `rule` }),
439
+ react.Children.toArray(props.children)
440
+ ]
441
+ }),
442
+ document.body
443
+ );
444
+ }
445
+ function InitializingChip({ t: t2, turn }) {
446
+ return h(
447
+ "span",
448
+ { className: turn ? "gdcc_turnRoot" : "gdcc_dockRoot", role: "status", "aria-busy": true },
449
+ h(
450
+ "span",
451
+ { className: (turn ? "gdcc_turnTrigger" : "gdcc_dockPill") + " gdcc_initializing", title: t2("initializing") },
452
+ [h(IconGaugeOutlineRegular, { key: "icon" }), h("span", { key: "label" }, t2("initializing"))]
453
+ )
454
+ );
455
+ }
456
+ function CostDockChip(props) {
457
+ const view = useCostView(props);
458
+ const dialog = useCostDialog();
459
+ const t2 = props.t;
460
+ if (view === void 0 || view === null || view.showDock === false) return null;
461
+ if (view.initializing) return h(InitializingChip, { t: t2 });
462
+ const label = t2(`costValue`, { amount: formatChipMoney(view.totalMicros, view.currency, view.cnyPerUsd) });
463
+ const buckets = view.bucketMicros ?? { input: 0, output: 0, read: 0, write: 0 };
464
+ const items = [
465
+ [t2(`input`), formatMoney(buckets.input, view.currency, view.cnyPerUsd)],
466
+ [t2(`cacheRead`), formatMoney(buckets.read, view.currency, view.cnyPerUsd)],
467
+ [t2(`cacheWrite`), formatMoney(buckets.write, view.currency, view.cnyPerUsd)],
468
+ [t2(`output`), formatMoney(buckets.output, view.currency, view.cnyPerUsd)]
469
+ ];
470
+ if (view.skippedTurns > 0) items.push([t2(`skipped`), t2(`unpricedValue`, { count: view.skippedTurns })]);
471
+ if (view.unpricedTokens > 0) items.push([t2(`unpriced`), t2(`count`, { count: view.unpricedTokens })]);
472
+ const routes = view.routes ?? [];
473
+ const routeRows = routes.length === 0 ? null : h(`div`, { key: `routes` }, [
474
+ h(`div`, { className: `gdcc_sub`, key: `h` }, t2(`byRoute`)),
475
+ details(
476
+ routes.flatMap((entry) => [
477
+ [
478
+ entry.route,
479
+ formatMoney(entry.micros, view.currency, view.cnyPerUsd) + ` \xB7 ` + t2(`count`, { count: formatExactTokens(entry.totalTokens) })
480
+ ]
481
+ ]),
482
+ `routes`
483
+ )
484
+ ]);
485
+ return h(`span`, { className: `gdcc_dockRoot`, ref: dialog.rootRef }, [
486
+ h(
487
+ `button`,
488
+ {
489
+ key: `b`,
490
+ type: `button`,
491
+ className: `gdcc_dockPill`,
492
+ "aria-haspopup": `dialog`,
493
+ "aria-expanded": dialog.open,
494
+ onClick: () => dialog.setOpen(!dialog.open)
495
+ },
496
+ [h(IconGaugeOutlineRegular, { key: `i` }), h(`span`, { className: `gdcc_dockLabel`, key: `l` }, label)]
497
+ ),
498
+ dialog.open ? panel({
499
+ panelRef: dialog.panelRef,
500
+ pos: dialog.pos,
501
+ title: t2(`sessionTitle`),
502
+ value: formatMoney(view.totalMicros, view.currency, view.cnyPerUsd),
503
+ icon: IconGaugeOutlineRegular,
504
+ children: [details(items, `main`), routeRows, h(`div`, { className: `gdcc_note`, key: `n` }, t2(`note`))]
505
+ }) : null
506
+ ]);
507
+ }
508
+ function CostTurnAction(props) {
509
+ const turn = props.useChat((snapshot) => {
510
+ const tail = snapshot.nodes.values().find((node) => {
511
+ var _a;
512
+ return node.kind === `turn-tail` && ((_a = node.data.closing) == null ? void 0 : _a.finalNode.messageId) === props.messageId;
513
+ });
514
+ return tail == null ? void 0 : tail.data.turn;
515
+ });
516
+ return turn === void 0 ? null : h(CostTurnChip, { ...props, turn });
517
+ }
518
+ function CostTurnChip(props) {
519
+ const view = useCostView(props);
520
+ const dialog = useCostDialog();
521
+ const t2 = props.t;
522
+ if (view === void 0 || view === null || view.showTurn === false) return null;
523
+ if (view.initializing) return h(InitializingChip, { t: t2, turn: true });
524
+ const raw = props.turn;
525
+ const turnNo = raw !== null && typeof raw === `object` ? raw.turn : raw;
526
+ if (!Number.isSafeInteger(turnNo) || turnNo < 1) return null;
527
+ const entry = view.turns === void 0 ? void 0 : view.turns[String(turnNo)];
528
+ if (entry === void 0) return null;
529
+ const label = entry.priced ? formatChipMoney(entry.micros, view.currency, view.cnyPerUsd) : t2(`noPricing`);
530
+ const priced = entry.priced === true;
531
+ const money = entry.bucketMicros;
532
+ const items = [
533
+ [t2(`model`), entry.route === `` ? null : entry.route],
534
+ priced ? [t2(`input`), formatMoney(money.input, view.currency, view.cnyPerUsd)] : [t2(`input`), t2(`noPricing`)],
535
+ priced ? [t2(`cacheRead`), formatMoney(money.read, view.currency, view.cnyPerUsd)] : null,
536
+ priced ? [t2(`cacheWrite`), formatMoney(money.write, view.currency, view.cnyPerUsd)] : null,
537
+ priced ? [t2(`output`), formatMoney(money.output, view.currency, view.cnyPerUsd)] : null
538
+ ].filter((item) => item !== null);
539
+ return h(`span`, { className: `gdcc_turnRoot`, ref: dialog.rootRef }, [
540
+ h(
541
+ `button`,
542
+ {
543
+ key: `b`,
544
+ type: `button`,
545
+ className: `gdcc_turnTrigger`,
546
+ "aria-haspopup": `dialog`,
547
+ "aria-expanded": dialog.open,
548
+ onClick: () => dialog.setOpen(!dialog.open)
549
+ },
550
+ [
551
+ h(IconGaugeOutlineRegular, { key: `i` }),
552
+ h(`span`, { className: `gdcc_turnLabel`, key: `l` }, t2(`turnCost`, { amount: label }))
553
+ ]
554
+ ),
555
+ dialog.open ? panel({
556
+ panelRef: dialog.panelRef,
557
+ pos: dialog.pos,
558
+ title: t2(`turnTitle`),
559
+ value: formatMoney(entry.micros, view.currency, view.cnyPerUsd),
560
+ icon: IconGaugeOutlineRegular,
561
+ children: [details(items, `turn`)]
562
+ }) : null
563
+ ]);
564
+ }
565
+ const zh = {
566
+ modelName: `\u6A21\u578B`,
567
+ initializing: `\u521D\u59CB\u5316\u4E2D`,
568
+ sessionTitle: `\u4F1A\u8BDD\u8D39\u7528`,
569
+ turnTitle: `\u672C\u8F6E\u8D39\u7528`,
570
+ costValue: `\u8D39\u7528 {amount}`,
571
+ turnCost: `{amount}`,
572
+ input: `\u672A\u7F13\u5B58\u8F93\u5165`,
573
+ cacheRead: `\u7F13\u5B58\u8BFB\u53D6`,
574
+ cacheWrite: `\u7F13\u5B58\u5199\u5165`,
575
+ output: `\u8F93\u51FA`,
576
+ model: `\u63D0\u4F9B\u65B9 / \u6A21\u578B`,
577
+ unpriced: `\u672A\u8BA1\u4EF7 token`,
578
+ unpricedValue: `{count} \u8F6E`,
579
+ skipped: `\u672A\u53D6\u5230\u7528\u91CF\u7684\u8F6E\u6B21`,
580
+ noPricing: `\u672A\u914D\u7F6E\u5355\u4EF7`,
581
+ byRoute: `\u6309\u6A21\u578B`,
582
+ count: `{count} tok`,
583
+ note: `\u8D39\u7528\u6309\u5404\u6A21\u578B\u5355\u4EF7\u4F30\u7B97,\u5355\u4EF7\u53EF\u5728\u63D2\u4EF6\u8BE6\u60C5\u9875\u7684\u8D39\u7528\u914D\u7F6E\u4E2D\u4FEE\u6539\u3002`,
584
+ pluginTitle: `\u8D39\u7528`,
585
+ pluginDescription: `\u6309 token \u7528\u91CF\u4F30\u7B97\u4F1A\u8BDD\u4E0E\u6BCF\u8F6E\u8D39\u7528,\u53EF\u81EA\u5B9A\u4E49\u5404\u6A21\u578B\u5355\u4EF7\u3002`,
586
+ configSectionGeneral: `\u901A\u7528`,
587
+ configCurrency: `\u663E\u793A\u8D27\u5E01`,
588
+ configCnyPerUsd: `\u6C47\u7387(1 USD = ? CNY)`,
589
+ configShowDockChip: `\u663E\u793A\u4F1A\u8BDD\u8D39\u7528\u80F6\u56CA`,
590
+ configShowTurnChip: `\u663E\u793A\u8F6E\u6B21\u8D39\u7528\u80F6\u56CA`,
591
+ configPricing: `\u6A21\u578B\u5355\u4EF7`,
592
+ configSave: `\u4FDD\u5B58`,
593
+ configSaving: `\u4FDD\u5B58\u4E2D\u2026`,
594
+ configSaved: `\u5DF2\u4FDD\u5B58`,
595
+ configLoading: `\u8BFB\u53D6\u914D\u7F6E\u4E2D\u2026`,
596
+ configLoadFailed: `\u8BFB\u53D6\u914D\u7F6E\u5931\u8D25:{message}`
597
+ };
598
+ const en = {
599
+ modelName: `Model`,
600
+ initializing: `Initializing`,
601
+ sessionTitle: `Session cost`,
602
+ turnTitle: `Turn cost`,
603
+ costValue: `Cost {amount}`,
604
+ turnCost: `{amount}`,
605
+ input: `Uncached input`,
606
+ cacheRead: `Cache read`,
607
+ cacheWrite: `Cache write`,
608
+ output: `Output`,
609
+ model: `Provider / model`,
610
+ unpriced: `Unpriced tokens`,
611
+ unpricedValue: `{count}`,
612
+ skipped: `Turns without usage`,
613
+ noPricing: `no rate configured`,
614
+ byRoute: `By model`,
615
+ count: `{count} tok`,
616
+ note: `Estimated from per-model rates; edit them on this plugin's detail page.`,
617
+ pluginTitle: `Cost`,
618
+ pluginDescription: `Estimates session and per-turn cost from token usage. Model rates are editable.`,
619
+ configSectionGeneral: `General`,
620
+ configCurrency: `Currency`,
621
+ configCnyPerUsd: `Rate (1 USD = ? CNY)`,
622
+ configShowDockChip: `Show session cost chip`,
623
+ configShowTurnChip: `Show per-turn cost chip`,
624
+ configPricing: `Model rates`,
625
+ configSave: `Save`,
626
+ configSaving: `Saving\u2026`,
627
+ configSaved: `Saved`,
628
+ configLoading: `Loading configuration\u2026`,
629
+ configLoadFailed: `Could not load configuration: {message}`
630
+ };
631
+ const ENTRY_ID = PACKAGE_ID;
632
+ function field(label, control, key) {
633
+ const value = control === null || control === void 0 ? null : react.cloneElement(control, { key: `v` });
634
+ return h(`div`, { className: `gdcc_field`, key }, [
635
+ h(`span`, { className: `gdcc_fieldLabel`, key: `l` }, label),
636
+ value
637
+ ]);
638
+ }
639
+ function numberInput(value, onChange, key) {
640
+ return h(`input`, {
641
+ key,
642
+ className: `gdcc_input`,
643
+ type: `number`,
644
+ step: `any`,
645
+ value: String(value ?? ``),
646
+ onChange: (event) => onChange(event.target.value)
647
+ });
648
+ }
649
+ function CostConfigCard(props) {
650
+ const t2 = props.t, text = (zh2, en2) => t2("configSave") === "\u4FDD\u5B58" ? zh2 : en2;
651
+ const [payload, setPayload] = react.useState(null), [draft, setDraft] = react.useState(null);
652
+ const [busy, setBusy] = react.useState(false);
653
+ const [error, setError] = react.useState(null), [notice, setNotice] = react.useState("");
654
+ const accept = (value) => {
655
+ setPayload(value);
656
+ setDraft(buildDraft(value));
657
+ };
658
+ react.useEffect(() => {
659
+ let active = true;
660
+ store.load().then((v) => active && accept(v)).catch((e2) => active && setError(e2.message));
661
+ return () => {
662
+ active = false;
663
+ };
664
+ }, []);
665
+ if (!draft) return h("div", { className: error ? "gdcc_error" : "gdcc_hint" }, error || t2("configLoading"));
666
+ const dirty = JSON.stringify(draft) !== JSON.stringify(buildDraft(payload));
667
+ const patch = (key, value) => setDraft((current) => ({ ...current, [key]: value }));
668
+ const rowPatch = (index, changes) => setDraft((current) => ({
669
+ ...current,
670
+ rows: current.rows.map((row2, at) => at === index ? { ...row2, ...changes } : row2)
671
+ }));
672
+ const run = async (sync) => {
673
+ if (sync && !window.confirm(
674
+ text(
675
+ "\u540C\u6B65\u5C06\u8986\u76D6\u914D\u7F6E\u4E2D\u5339\u914D\u6A21\u578B\u7684\u4EF7\u683C\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F",
676
+ "Sync will overwrite prices of matched configured models. Continue?"
677
+ )
678
+ ))
679
+ return;
680
+ setBusy(true);
681
+ setError(null);
682
+ setNotice("");
683
+ try {
684
+ const value = sync ? await store.sync() : await store.save(configOf(draft));
685
+ accept(value);
686
+ const skipped = value.report ? [...value.report.unmatched, ...value.report.unsupported, ...value.report.ambiguous] : [];
687
+ setNotice(
688
+ value.report ? text("\u5DF2\u66F4\u65B0", "Updated") + ": " + value.report.updated.length + "; " + text("\u4FDD\u7559\u539F\u4EF7", "Preserved") + ": " + skipped.length + (skipped.length ? " (" + skipped.join(", ") + ")" : "") : t2("configSaved")
689
+ );
690
+ } catch (e2) {
691
+ setError(e2.message);
692
+ } finally {
693
+ setBusy(false);
694
+ }
695
+ };
696
+ const groups = /* @__PURE__ */ new Map();
697
+ draft.rows.forEach((row2, index) => {
698
+ const { provider, model } = parseRouteKey(row2.key);
699
+ if (!groups.has(provider)) groups.set(provider, []);
700
+ groups.get(provider).push({ row: row2, index, model });
701
+ });
702
+ const rows = [...groups].flatMap(([provider, entries]) => [
703
+ h(
704
+ "div",
705
+ { key: `provider:${provider}`, className: "gdcc_provider", role: "heading", "aria-level": 3 },
706
+ provider
707
+ ),
708
+ ...entries.map(
709
+ ({ row: row2, index, model }) => h("div", { className: "gdcc_row", key: row2.key }, [
710
+ h("span", { key: "model", className: "gdcc_model", title: row2.key }, model),
711
+ ...RATE_KEYS.map((key) => numberInput(row2[key], (value) => rowPatch(index, { [key]: value }), key)),
712
+ h(
713
+ "button",
714
+ {
715
+ key: "reset",
716
+ type: "button",
717
+ className: "gdcc_btn",
718
+ title: text("\u4EF7\u683C\u5F52\u96F6", "Reset prices to zero"),
719
+ onClick: () => rowPatch(index, zeroRate())
720
+ },
721
+ "\u21BA"
722
+ )
723
+ ])
724
+ )
725
+ ]);
726
+ return h("fieldset", { className: "gdcc_form", disabled: busy }, [
727
+ field(
728
+ t2("configCurrency"),
729
+ h(
730
+ "select",
731
+ { className: "gdcc_input", value: draft.currency, onChange: (e2) => patch("currency", e2.target.value) },
732
+ ["USD", "CNY"].map((value) => h("option", { key: value, value }, value))
733
+ ),
734
+ "currency"
735
+ ),
736
+ field(
737
+ t2("configCnyPerUsd"),
738
+ numberInput(draft.cnyPerUsd, (value) => patch("cnyPerUsd", value)),
739
+ "exchange"
740
+ ),
741
+ ...["showDockChip", "showTurnChip"].map(
742
+ (key) => field(
743
+ t2(key === "showDockChip" ? "configShowDockChip" : "configShowTurnChip"),
744
+ h("input", {
745
+ className: "gdcc_input",
746
+ type: "checkbox",
747
+ checked: draft[key],
748
+ onChange: (e2) => patch(key, e2.target.checked)
749
+ }),
750
+ key
751
+ )
752
+ ),
753
+ h("div", { key: "pricing", className: "gdcc_sub" }, t2("configPricing") + " (USD / 1M tokens)"),
754
+ h("div", { key: "source", className: "gdcc_actions" }, [
755
+ h("span", { key: "from" }, text("\u4ECE", "From")),
756
+ h(
757
+ "select",
758
+ {
759
+ key: "select",
760
+ className: "gdcc_input",
761
+ style: { width: "auto" },
762
+ "aria-label": text("\u4EF7\u683C\u6765\u6E90", "Price source")
763
+ },
764
+ h("option", { value: "models.dev" }, "models.dev")
765
+ ),
766
+ h(
767
+ "button",
768
+ { key: "sync", type: "button", className: "gdcc_btn", disabled: dirty || busy, onClick: () => run(true) },
769
+ text("\u540C\u6B65\u4EF7\u683C", "Sync prices")
770
+ )
771
+ ]),
772
+ dirty ? h(
773
+ "div",
774
+ { key: "dirty", className: "gdcc_hint" },
775
+ text("\u8BF7\u5148\u4FDD\u5B58\u4FEE\u6539\uFF0C\u518D\u540C\u6B65\u4EF7\u683C\u3002", "Save changes before syncing prices.")
776
+ ) : null,
777
+ h("div", { key: "rows", className: "gdcc_rows" }, [
778
+ h(
779
+ "div",
780
+ { key: "head", className: "gdcc_row" },
781
+ ["modelName", "input", "output", "cacheRead", "cacheWrite", ""].map(
782
+ (key) => h("span", { key, className: "gdcc_rowHead" }, key ? t2(key) : "")
783
+ )
784
+ ),
785
+ ...rows
786
+ ]),
787
+ draft.rows.length ? null : h("div", { key: "empty", className: "gdcc_hint" }, text("\u6682\u65E0\u6A21\u578B\u8C03\u7528\u5386\u53F2", "No model call history")),
788
+ h("div", { key: "actions", className: "gdcc_actions" }, [
789
+ error ? h("span", { key: "error", className: "gdcc_error" }, error) : null,
790
+ notice ? h("span", { key: "notice", className: "gdcc_ok" }, notice) : null,
791
+ h(
792
+ "button",
793
+ {
794
+ key: "reload",
795
+ type: "button",
796
+ className: "gdcc_btn",
797
+ onClick: async () => {
798
+ setBusy(true);
799
+ try {
800
+ accept(await store.load());
801
+ setError(null);
802
+ } catch (e2) {
803
+ setError(e2.message);
804
+ } finally {
805
+ setBusy(false);
806
+ }
807
+ }
808
+ },
809
+ text("\u91CD\u65B0\u52A0\u8F7D", "Reload")
810
+ ),
811
+ h(
812
+ "button",
813
+ { key: "save", type: "button", className: "gdcc_btn gdcc_btnPrimary", onClick: () => run(false) },
814
+ busy ? t2("configSaving") : t2("configSave")
815
+ )
816
+ ])
817
+ ]);
818
+ }
819
+ const inject = [`slots`, `locale`];
820
+ function apply(ctx) {
821
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), `guojinpeng/dsh-cost-meter: dictionaries`);
822
+ ctx.slots.inject(
823
+ `conversation.composer.dock`,
824
+ () => ctx.slots.register(
825
+ {
826
+ name: `conversation.composer.dock`,
827
+ id: `${PLUGIN_ID}/dock`,
828
+ order: 10,
829
+ locale: NS
830
+ },
831
+ CostDockChip
832
+ )
833
+ );
834
+ ctx.slots.inject(
835
+ `conversation.chat.assistant-actions`,
836
+ () => ctx.slots.register(
837
+ {
838
+ name: `conversation.chat.assistant-actions`,
839
+ id: `${PLUGIN_ID}/turn`,
840
+ order: 10,
841
+ locale: NS
842
+ },
843
+ CostTurnAction
844
+ )
845
+ );
846
+ ctx.effect(
847
+ () => ctx.slots.inject(
848
+ `plugins.bundle.config`,
849
+ () => ctx.slots.register(
850
+ {
851
+ name: `plugins.bundle.config`,
852
+ key: ENTRY_ID,
853
+ locale: NS,
854
+ inject: () => ({})
855
+ },
856
+ CostConfigCard
857
+ )
858
+ ),
859
+ `guojinpeng/dsh-cost-meter: bundle config page`
860
+ );
861
+ }
862
+ n.apply = apply;
863
+ n.inject = inject;
864
+ return t.exports;
865
+ }
866
+ });