@gamegeek-saikel/dsh-cost-meter 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 +116 -0
- package/README.zh-CN.md +116 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +1481 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +2403 -0
- package/lib/invariant.js +38 -0
- package/lib/types/client/AssistantCostChip.d.ts +29 -0
- package/lib/types/client/AssistantCostChip.d.ts.map +1 -0
- package/lib/types/client/AssistantCostChip.js +75 -0
- package/lib/types/client/CostPluginCard.d.ts +10 -0
- package/lib/types/client/CostPluginCard.d.ts.map +1 -0
- package/lib/types/client/CostPluginCard.js +124 -0
- package/lib/types/client/CostView.d.ts +10 -0
- package/lib/types/client/CostView.d.ts.map +1 -0
- package/lib/types/client/CostView.js +66 -0
- package/lib/types/client/SessionCostLine.d.ts +21 -0
- package/lib/types/client/SessionCostLine.d.ts.map +1 -0
- package/lib/types/client/SessionCostLine.js +173 -0
- package/lib/types/client/SessionCostPill.d.ts +13 -0
- package/lib/types/client/SessionCostPill.d.ts.map +1 -0
- package/lib/types/client/SessionCostPill.js +123 -0
- package/lib/types/client/cost-math.d.ts +88 -0
- package/lib/types/client/cost-math.d.ts.map +1 -0
- package/lib/types/client/cost-math.js +199 -0
- package/lib/types/client/format.d.ts +35 -0
- package/lib/types/client/format.d.ts.map +1 -0
- package/lib/types/client/format.js +61 -0
- package/lib/types/client/index.d.ts +28 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +59 -0
- package/lib/types/client/locales.d.ts +78 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/locales.js +149 -0
- package/lib/types/index.d.ts +136 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +401 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/invariant.js +38 -0
- package/lib/types/pricebook.d.ts +467 -0
- package/lib/types/pricebook.d.ts.map +1 -0
- package/lib/types/pricebook.js +696 -0
- package/lib/types/pricing.d.ts +93 -0
- package/lib/types/pricing.d.ts.map +1 -0
- package/lib/types/pricing.js +346 -0
- package/lib/types/session-cost-projection.d.ts +70 -0
- package/lib/types/session-cost-projection.d.ts.map +1 -0
- package/lib/types/session-cost-projection.js +198 -0
- package/lib/types/subagent-cost.d.ts +51 -0
- package/lib/types/subagent-cost.d.ts.map +1 -0
- package/lib/types/subagent-cost.js +79 -0
- package/lib/types/types.d.ts +334 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +6 -0
- package/package.json +134 -0
- package/src/client/AssistantCostChip.module.css +81 -0
- package/src/client/AssistantCostChip.tsx +109 -0
- package/src/client/CostPluginCard.module.css +186 -0
- package/src/client/CostPluginCard.tsx +264 -0
- package/src/client/CostView.module.css +168 -0
- package/src/client/CostView.tsx +145 -0
- package/src/client/SessionCostLine.module.css +19 -0
- package/src/client/SessionCostLine.tsx +186 -0
- package/src/client/SessionCostPill.module.css +118 -0
- package/src/client/SessionCostPill.tsx +192 -0
- package/src/client/cost-math.ts +230 -0
- package/src/client/format.ts +63 -0
- package/src/client/index.ts +108 -0
- package/src/client/locales.ts +165 -0
- package/src/css-modules.d.ts +4 -0
- package/src/index.ts +489 -0
- package/src/invariant.ts +47 -0
- package/src/pricebook.ts +825 -0
- package/src/pricing.ts +378 -0
- package/src/session-cost-projection.ts +238 -0
- package/src/subagent-cost.ts +105 -0
- package/src/types.ts +364 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Session header pill (right-aligned utility): the conversation's anchored
|
|
4
|
+
* total cost, or — while the assistant is replying — a live estimate of the
|
|
5
|
+
* TOTAL once the running reply settles (`预计 ¥x.xx(进行中)`): the already
|
|
6
|
+
* anchored ledger total plus the running reply's estimated input/output.
|
|
7
|
+
* Clicking the pill toggles a detail panel: totals by category, model, and
|
|
8
|
+
* the anchored price snapshot; the panel dismisses on outside clicks or
|
|
9
|
+
* Escape.
|
|
10
|
+
*
|
|
11
|
+
* The estimate is explicitly labeled 估算/estimate: input is priced at the
|
|
12
|
+
* projected context size weighted by the newest completed step's cache
|
|
13
|
+
* ratio (conservative all-uncached without history), output at the
|
|
14
|
+
* character heuristic — both at CURRENT snapshot prices.
|
|
15
|
+
*/
|
|
16
|
+
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
17
|
+
import { bandForTime, blocksOutputTokens, bucketAt, cacheReadRatioOf, combineTotals, estimateCost, peakOffPeakMultiplier, subagentSpend } from "./cost-math.js";
|
|
18
|
+
import { currencySymbol, displayCurrency, formatMoney, formatMultiplier, formatTime } from "./format.js";
|
|
19
|
+
import { ENDPOINT, REFRESH_MS } from "./SessionCostLine.js";
|
|
20
|
+
import css from './SessionCostPill.module.css';
|
|
21
|
+
/** Read the conversation's running flag (the pill's estimate gate). */
|
|
22
|
+
function runningOf(snapshot) {
|
|
23
|
+
return snapshot.running;
|
|
24
|
+
}
|
|
25
|
+
/** Close the popover on outside clicks or Escape (product detail-panel posture). */
|
|
26
|
+
function useDismissOutside(open, onDismiss, anchor) {
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!open)
|
|
29
|
+
return;
|
|
30
|
+
const onPointerDown = (event) => {
|
|
31
|
+
const target = event.target;
|
|
32
|
+
if (anchor.current !== null && target !== null && !anchor.current.contains(target))
|
|
33
|
+
onDismiss();
|
|
34
|
+
};
|
|
35
|
+
const onKeyDown = (event) => {
|
|
36
|
+
if (event.key === 'Escape')
|
|
37
|
+
onDismiss();
|
|
38
|
+
};
|
|
39
|
+
document.addEventListener('mousedown', onPointerDown);
|
|
40
|
+
document.addEventListener('touchstart', onPointerDown);
|
|
41
|
+
document.addEventListener('keydown', onKeyDown);
|
|
42
|
+
return () => {
|
|
43
|
+
document.removeEventListener('mousedown', onPointerDown);
|
|
44
|
+
document.removeEventListener('touchstart', onPointerDown);
|
|
45
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
46
|
+
};
|
|
47
|
+
}, [open, onDismiss, anchor]);
|
|
48
|
+
}
|
|
49
|
+
export const SessionCostPill = memo(function SessionCostPill({ useSession, useProjection, sessionId, t }) {
|
|
50
|
+
const currency = displayCurrency(t);
|
|
51
|
+
const projectionKey = currency === 'USD' ? 'sessionCostUsd' : 'sessionCost';
|
|
52
|
+
const cost = useProjection(projectionKey);
|
|
53
|
+
const pressure = useProjection('contextPressure');
|
|
54
|
+
const running = useSession(runningOf);
|
|
55
|
+
const partial = useSession(snapshot => snapshot.partial);
|
|
56
|
+
const [open, setOpen] = useState(false);
|
|
57
|
+
const [response, setResponse] = useState(null);
|
|
58
|
+
const wrapRef = useRef(null);
|
|
59
|
+
useDismissOutside(open, () => setOpen(false), wrapRef);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
let alive = true;
|
|
62
|
+
let timer;
|
|
63
|
+
const endpoint = `${ENDPOINT}?session=${encodeURIComponent(sessionId)}¤cy=${currency}`;
|
|
64
|
+
const load = () => {
|
|
65
|
+
void fetch(endpoint, { cache: 'no-store' })
|
|
66
|
+
.then(res => (res.ok ? res.json() : null))
|
|
67
|
+
.then((data) => {
|
|
68
|
+
if (!alive || data === null)
|
|
69
|
+
return;
|
|
70
|
+
setResponse(data);
|
|
71
|
+
})
|
|
72
|
+
.catch(() => { });
|
|
73
|
+
};
|
|
74
|
+
load();
|
|
75
|
+
timer = window.setInterval(load, REFRESH_MS);
|
|
76
|
+
return () => {
|
|
77
|
+
alive = false;
|
|
78
|
+
if (timer !== undefined)
|
|
79
|
+
window.clearInterval(timer);
|
|
80
|
+
};
|
|
81
|
+
}, [sessionId, currency]);
|
|
82
|
+
const projection = cost;
|
|
83
|
+
const model = projection?.model ?? null;
|
|
84
|
+
const snapshot = response?.pricebook.current ?? null;
|
|
85
|
+
const subagents = response?.subagents;
|
|
86
|
+
const combined = combineTotals(projection?.totals, subagents);
|
|
87
|
+
const subSpend = subagentSpend(subagents);
|
|
88
|
+
const symbol = currencySymbol(currency);
|
|
89
|
+
// The estimate projects the TOTAL once the running reply settles: the
|
|
90
|
+
// already-anchored ledger total (main + subagents) plus this reply's
|
|
91
|
+
// estimated input/output (subagent replies cannot be projected live).
|
|
92
|
+
const estimate = useMemo(() => {
|
|
93
|
+
if (!running || projection === undefined)
|
|
94
|
+
return null;
|
|
95
|
+
const bucket = bucketAt(snapshot ?? undefined, model?.provider, model?.model ?? 'unknown', Date.now());
|
|
96
|
+
if (bucket === undefined)
|
|
97
|
+
return null;
|
|
98
|
+
const inputTokens = pressure?.projectedTokens ?? pressure?.pressureTokens ?? 0;
|
|
99
|
+
const cachedRatio = cacheReadRatioOf(projection.steps);
|
|
100
|
+
const outputTokens = blocksOutputTokens(partial?.blocks);
|
|
101
|
+
const stepEstimate = estimateCost(bucket, inputTokens, cachedRatio, outputTokens);
|
|
102
|
+
return (combined?.cost ?? 0) + stepEstimate;
|
|
103
|
+
}, [running, projection, combined, snapshot, model, pressure, partial, currency]);
|
|
104
|
+
if (projection === undefined && estimate === null)
|
|
105
|
+
return null;
|
|
106
|
+
const total = combined?.cost ?? 0;
|
|
107
|
+
const label = estimate !== null
|
|
108
|
+
? t('pill.estimate', { amount: `${symbol}${formatMoney(estimate)}` })
|
|
109
|
+
: t('pill.label', { amount: `${symbol}${formatMoney(total)}` });
|
|
110
|
+
const last = projection?.steps.length !== undefined && projection.steps.length > 0
|
|
111
|
+
? projection.steps[projection.steps.length - 1]
|
|
112
|
+
: undefined;
|
|
113
|
+
const sourceKey = snapshot?.source ?? null;
|
|
114
|
+
const source = sourceKey === null ? t('source.none') : t(`source.${sourceKey}`);
|
|
115
|
+
const band = bandForTime(Date.now());
|
|
116
|
+
const peakRatio = peakOffPeakMultiplier(snapshot ?? undefined, model?.provider, model?.model ?? 'unknown');
|
|
117
|
+
const bandLabel = band === 'peak'
|
|
118
|
+
? peakRatio === null ? t('band.peak') : t('price.peakRatio', { multiplier: formatMultiplier(peakRatio) })
|
|
119
|
+
: band === 'offPeak'
|
|
120
|
+
? peakRatio === null ? t('band.offPeak') : t('price.offPeakRatio', { multiplier: formatMultiplier(1 / peakRatio) })
|
|
121
|
+
: null;
|
|
122
|
+
return (_jsxs("div", { className: css.wrap, ref: wrapRef, children: [_jsxs("button", { type: "button", className: css.pill, "data-testid": "cost-pill", "data-running": estimate !== null, "data-band": band === 'single' ? undefined : band, onClick: () => setOpen(prev => !prev), "aria-expanded": open, children: [label, bandLabel !== null && _jsx("span", { className: css.bandBadge, "data-testid": "cost-pill-band", children: bandLabel }), estimate !== null && _jsxs("span", { className: css.estimateNote, children: ["\u00B7 ", t('pill.estimateNote')] })] }), open && (_jsxs("div", { className: css.detail, "data-testid": "cost-pill-detail", children: [_jsx("div", { className: css.detailRow + ' ' + css.detailTotal, children: t('pill.detail.total', { amount: `${symbol}${formatMoney(total)}` }) }), bandLabel !== null && (_jsx("div", { className: css.detailRow + ' ' + css.detailRowSecondary, "data-testid": "cost-pill-detail-band", children: bandLabel })), combined !== undefined && combined.pricedSteps > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.detailRow + ' ' + css.detailRowSecondary, children: [t('view.uncached'), ": ", symbol, formatMoney(combined.uncachedCost)] }), _jsxs("div", { className: css.detailRow + ' ' + css.detailRowSecondary, children: [t('view.cacheRead'), ": ", symbol, formatMoney(combined.cacheReadCost)] }), _jsxs("div", { className: css.detailRow + ' ' + css.detailRowSecondary, children: [t('view.output'), ": ", symbol, formatMoney(combined.outputCost)] })] })), subSpend > 0 && (_jsx("div", { className: css.detailRow + ' ' + css.detailRowMuted, children: t('line.subagent', { amount: `${symbol}${formatMoney(subSpend)}` }) })), model !== null && (_jsx("div", { className: css.detailRow + ' ' + css.detailRowSecondary, children: t('pill.detail.model', { model: model.model }) })), last !== undefined && last.snapshotVersion !== null && (_jsxs("div", { className: css.detailRow + ' ' + css.detailRowMuted, children: [t('pill.detail.snapshot', { time: formatTime(last.snapshotEffectiveAt ?? last.time) }), last.band !== null && ` · ${t(`band.${last.band}`)}`] })), _jsx("div", { className: css.detailRow + ' ' + css.detailRowMuted, children: t('pill.detail.source', { source }) }), projection !== undefined && projection.totals.unpricedSteps > 0 && (_jsx("div", { className: css.detailRow + ' ' + css.detailRowMuted, children: t('view.unpriced', { count: String(projection.totals.unpricedSteps) }) })), estimate !== null && (_jsx("div", { className: css.detailRow + ' ' + css.detailRowMuted, children: t('pill.estimateNote') }))] }))] }));
|
|
123
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side price lookup and streaming-estimate helpers.
|
|
3
|
+
*
|
|
4
|
+
* The authoritative anchored ledger arrives through the `sessionCost`
|
|
5
|
+
* projection; this module only mirrors the host pricebook math for display
|
|
6
|
+
* and for the live estimate (which by nature uses CURRENT prices and is
|
|
7
|
+
* labeled 估算/estimate). The lookup logic intentionally mirrors
|
|
8
|
+
* `src/pricebook.ts` — keep the two in sync.
|
|
9
|
+
*
|
|
10
|
+
* @module @gamegeek-saikel/dsh-cost-meter/client/cost-math
|
|
11
|
+
*/
|
|
12
|
+
import type { ModelPrice, PriceBand, PriceBucket, PricebookSnapshot, SessionCostStep, SessionCostTotals, SubagentCost } from '../types.ts';
|
|
13
|
+
/** Sum totals (this module's mirror of the host `sumTotals`). */
|
|
14
|
+
export declare function sumTotals(totals: readonly SessionCostTotals[]): SessionCostTotals;
|
|
15
|
+
/** Total spend of every subagent (CNY); 0 without subagents. */
|
|
16
|
+
export declare function subagentSpend(subagents: readonly SubagentCost[] | undefined): number;
|
|
17
|
+
/**
|
|
18
|
+
* The conversation-wide totals: the main session's ledger plus every
|
|
19
|
+
* subagent's ledger (host-aggregated). Undefined while the main ledger is
|
|
20
|
+
* absent; identical to the main totals without subagents.
|
|
21
|
+
* @param main - the main session's projection totals.
|
|
22
|
+
* @param subagents - the host-aggregated subagent totals.
|
|
23
|
+
* @returns the combined totals, or undefined.
|
|
24
|
+
*/
|
|
25
|
+
export declare function combineTotals(main: SessionCostTotals | undefined, subagents: readonly SubagentCost[] | undefined): SessionCostTotals | undefined;
|
|
26
|
+
/** The price band in effect at one instant (mirror of the host helper). */
|
|
27
|
+
export declare function bandForTime(time: number): PriceBand;
|
|
28
|
+
/** The bucket of one model's entry for one band (mirror of the host helper). */
|
|
29
|
+
export declare function bucketOf(modelPrice: ModelPrice, band: PriceBand): PriceBucket | undefined;
|
|
30
|
+
/** Map a model id to its official pricing key (mirror of the host helper). */
|
|
31
|
+
export declare function pricingKeyOfModel(model: string): 'pro' | 'flash';
|
|
32
|
+
/** Candidate canonical price keys for one model (mirror of the host helper). */
|
|
33
|
+
export declare function modelKeys(provider: string | undefined, model: string): string[];
|
|
34
|
+
/** Pick the snapshot effective at `time` (newest ≤ time, else earliest). */
|
|
35
|
+
export declare function snapshotAt(snapshots: readonly PricebookSnapshot[], time: number): PricebookSnapshot | undefined;
|
|
36
|
+
/** Resolve one model's price entry in a snapshot through the key chain. */
|
|
37
|
+
export declare function modelPriceOf(snapshot: PricebookSnapshot | null | undefined, provider: string | undefined, model: string): ModelPrice | undefined;
|
|
38
|
+
/** Resolve one model's bucket at one instant from a snapshot. */
|
|
39
|
+
export declare function bucketAt(snapshot: PricebookSnapshot | undefined, provider: string | undefined, model: string, time: number): PriceBucket | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The peak-to-off-peak price multiplier for one model's entry, when the
|
|
42
|
+
* entry carries both bands. The multiplier is the ratio of the summed peak
|
|
43
|
+
* bucket rates to the summed off-peak bucket rates; official DeepSeek prices
|
|
44
|
+
* scale all buckets by the same factor, so this is a stable “peak is N× the
|
|
45
|
+
* off-peak price” display value.
|
|
46
|
+
* @param snapshot - the pricebook snapshot to read.
|
|
47
|
+
* @param provider - the provider route, when known.
|
|
48
|
+
* @param model - the model id.
|
|
49
|
+
* @returns the peak/off-peak multiplier, or null when either band is absent.
|
|
50
|
+
*/
|
|
51
|
+
export declare function peakOffPeakMultiplier(snapshot: PricebookSnapshot | null | undefined, provider: string | undefined, model: string): number | null;
|
|
52
|
+
/**
|
|
53
|
+
* Heuristic character→token estimate: CJK ÷ 1.5, everything else ÷ 4
|
|
54
|
+
* (the streaming estimate's output side).
|
|
55
|
+
* @param text - the accumulated text.
|
|
56
|
+
* @returns the estimated token count.
|
|
57
|
+
*/
|
|
58
|
+
export declare function estimateTokens(text: string): number;
|
|
59
|
+
/**
|
|
60
|
+
* Tokens of the streamed text/reasoning blocks of a partial assistant.
|
|
61
|
+
* @param blocks - the partial's content blocks (text/reasoning carry `text`).
|
|
62
|
+
* @returns the estimated token count.
|
|
63
|
+
*/
|
|
64
|
+
export declare function blocksOutputTokens(blocks: readonly {
|
|
65
|
+
kind?: string;
|
|
66
|
+
text?: string;
|
|
67
|
+
}[] | undefined): number;
|
|
68
|
+
/**
|
|
69
|
+
* The cache-read ratio of the newest completed step (input side of the
|
|
70
|
+
* estimate). Absent history conservatively prices all input as uncached
|
|
71
|
+
* (upper bound).
|
|
72
|
+
* @param steps - the projection's step ledger.
|
|
73
|
+
* @returns the ratio in [0, 1].
|
|
74
|
+
*/
|
|
75
|
+
export declare function cacheReadRatioOf(steps: readonly SessionCostStep[]): number;
|
|
76
|
+
/**
|
|
77
|
+
* The live estimate of the running reply: input at the projected context
|
|
78
|
+
* size (weighted by the cache ratio) plus output at the streamed-token
|
|
79
|
+
* heuristic, both at CURRENT prices. Always labeled 估算/estimate — it is
|
|
80
|
+
* replaced by the projection's exact anchored value once the step settles.
|
|
81
|
+
* @param bucket - the current price bucket of the session model.
|
|
82
|
+
* @param inputTokens - the projected prompt size for the next request.
|
|
83
|
+
* @param cachedRatio - the cache-read ratio from recent completed steps.
|
|
84
|
+
* @param outputTokens - the streamed output tokens so far.
|
|
85
|
+
* @returns the estimated CNY cost.
|
|
86
|
+
*/
|
|
87
|
+
export declare function estimateCost(bucket: PriceBucket, inputTokens: number, cachedRatio: number, outputTokens: number): number;
|
|
88
|
+
//# sourceMappingURL=cost-math.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-math.d.ts","sourceRoot":"","sources":["../../../src/client/cost-math.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EACV,UAAU,EACV,SAAS,EACT,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,YAAY,EACb,MAAM,aAAa,CAAA;AAEpB,iEAAiE;AACjE,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,GAAG,iBAAiB,CAoBjF;AAED,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,SAAS,EAAE,SAAS,YAAY,EAAE,GAAG,SAAS,GAAG,MAAM,CAGpF;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,iBAAiB,GAAG,SAAS,EACnC,SAAS,EAAE,SAAS,YAAY,EAAE,GAAG,SAAS,GAC7C,iBAAiB,GAAG,SAAS,CAI/B;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAGnD;AAED,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,CAIzF;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,CAGhE;AAED,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAS/E;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,SAAS,EAAE,SAAS,iBAAiB,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAO/G;AAED,2EAA2E;AAC3E,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,EAC9C,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,GACZ,UAAU,GAAG,SAAS,CAOxB;AAED,iEAAiE;AACjE,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GACX,WAAW,GAAG,SAAS,CAIzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,EAC9C,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,IAAI,CAUf;AAKD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,SAAS,GAAG,MAAM,CAQ1G;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,GAAG,MAAM,CAO1E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,MAAM,CAKR"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side price lookup and streaming-estimate helpers.
|
|
3
|
+
*
|
|
4
|
+
* The authoritative anchored ledger arrives through the `sessionCost`
|
|
5
|
+
* projection; this module only mirrors the host pricebook math for display
|
|
6
|
+
* and for the live estimate (which by nature uses CURRENT prices and is
|
|
7
|
+
* labeled 估算/estimate). The lookup logic intentionally mirrors
|
|
8
|
+
* `src/pricebook.ts` — keep the two in sync.
|
|
9
|
+
*
|
|
10
|
+
* @module @gamegeek-saikel/dsh-cost-meter/client/cost-math
|
|
11
|
+
*/
|
|
12
|
+
import { isPeakHour, PEAK_PRICING_START_MS } from "../pricing.js";
|
|
13
|
+
/** Sum totals (this module's mirror of the host `sumTotals`). */
|
|
14
|
+
export function sumTotals(totals) {
|
|
15
|
+
const sum = {
|
|
16
|
+
uncachedCost: 0,
|
|
17
|
+
cacheReadCost: 0,
|
|
18
|
+
outputCost: 0,
|
|
19
|
+
cost: 0,
|
|
20
|
+
pricedSteps: 0,
|
|
21
|
+
unpricedSteps: 0,
|
|
22
|
+
steps: 0,
|
|
23
|
+
};
|
|
24
|
+
for (const entry of totals) {
|
|
25
|
+
sum.uncachedCost += entry.uncachedCost;
|
|
26
|
+
sum.cacheReadCost += entry.cacheReadCost;
|
|
27
|
+
sum.outputCost += entry.outputCost;
|
|
28
|
+
sum.cost += entry.cost;
|
|
29
|
+
sum.pricedSteps += entry.pricedSteps;
|
|
30
|
+
sum.unpricedSteps += entry.unpricedSteps;
|
|
31
|
+
sum.steps += entry.steps;
|
|
32
|
+
}
|
|
33
|
+
return sum;
|
|
34
|
+
}
|
|
35
|
+
/** Total spend of every subagent (CNY); 0 without subagents. */
|
|
36
|
+
export function subagentSpend(subagents) {
|
|
37
|
+
if (subagents === undefined || subagents.length === 0)
|
|
38
|
+
return 0;
|
|
39
|
+
return sumTotals(subagents.map(entry => entry.totals)).cost;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The conversation-wide totals: the main session's ledger plus every
|
|
43
|
+
* subagent's ledger (host-aggregated). Undefined while the main ledger is
|
|
44
|
+
* absent; identical to the main totals without subagents.
|
|
45
|
+
* @param main - the main session's projection totals.
|
|
46
|
+
* @param subagents - the host-aggregated subagent totals.
|
|
47
|
+
* @returns the combined totals, or undefined.
|
|
48
|
+
*/
|
|
49
|
+
export function combineTotals(main, subagents) {
|
|
50
|
+
if (main === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (subagents === undefined || subagents.length === 0)
|
|
53
|
+
return main;
|
|
54
|
+
return sumTotals([main, ...subagents.map(entry => entry.totals)]);
|
|
55
|
+
}
|
|
56
|
+
/** The price band in effect at one instant (mirror of the host helper). */
|
|
57
|
+
export function bandForTime(time) {
|
|
58
|
+
if (time < PEAK_PRICING_START_MS)
|
|
59
|
+
return 'single';
|
|
60
|
+
return isPeakHour(new Date(time)) ? 'peak' : 'offPeak';
|
|
61
|
+
}
|
|
62
|
+
/** The bucket of one model's entry for one band (mirror of the host helper). */
|
|
63
|
+
export function bucketOf(modelPrice, band) {
|
|
64
|
+
if (band === 'single')
|
|
65
|
+
return modelPrice.single ?? modelPrice.offPeak ?? modelPrice.peak;
|
|
66
|
+
if (band === 'peak')
|
|
67
|
+
return modelPrice.peak ?? modelPrice.offPeak ?? modelPrice.single;
|
|
68
|
+
return modelPrice.offPeak ?? modelPrice.peak ?? modelPrice.single;
|
|
69
|
+
}
|
|
70
|
+
/** Map a model id to its official pricing key (mirror of the host helper). */
|
|
71
|
+
export function pricingKeyOfModel(model) {
|
|
72
|
+
const lower = model.toLowerCase();
|
|
73
|
+
return lower.includes('pro') ? 'pro' : 'flash';
|
|
74
|
+
}
|
|
75
|
+
/** Candidate canonical price keys for one model (mirror of the host helper). */
|
|
76
|
+
export function modelKeys(provider, model) {
|
|
77
|
+
const keys = [];
|
|
78
|
+
if (provider !== undefined && provider.length > 0)
|
|
79
|
+
keys.push(`${provider}/${model}`);
|
|
80
|
+
keys.push(model);
|
|
81
|
+
const lower = model.toLowerCase();
|
|
82
|
+
const deepseekish = lower.includes('deepseek')
|
|
83
|
+
|| (provider !== undefined && provider.toLowerCase().includes('deepseek'));
|
|
84
|
+
if (deepseekish)
|
|
85
|
+
keys.push(pricingKeyOfModel(model));
|
|
86
|
+
return keys;
|
|
87
|
+
}
|
|
88
|
+
/** Pick the snapshot effective at `time` (newest ≤ time, else earliest). */
|
|
89
|
+
export function snapshotAt(snapshots, time) {
|
|
90
|
+
let best;
|
|
91
|
+
for (const snapshot of snapshots) {
|
|
92
|
+
// `>=` keeps the NEWER snapshot (later in the array) on an effectiveAt tie.
|
|
93
|
+
if (snapshot.effectiveAt <= time && (best === undefined || snapshot.effectiveAt >= best.effectiveAt))
|
|
94
|
+
best = snapshot;
|
|
95
|
+
}
|
|
96
|
+
return best ?? (snapshots.length > 0 ? snapshots[0] : undefined);
|
|
97
|
+
}
|
|
98
|
+
/** Resolve one model's price entry in a snapshot through the key chain. */
|
|
99
|
+
export function modelPriceOf(snapshot, provider, model) {
|
|
100
|
+
if (snapshot === undefined || snapshot === null)
|
|
101
|
+
return undefined;
|
|
102
|
+
for (const key of modelKeys(provider, model)) {
|
|
103
|
+
const entry = snapshot.prices[key];
|
|
104
|
+
if (entry !== undefined)
|
|
105
|
+
return entry;
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
/** Resolve one model's bucket at one instant from a snapshot. */
|
|
110
|
+
export function bucketAt(snapshot, provider, model, time) {
|
|
111
|
+
const entry = modelPriceOf(snapshot, provider, model);
|
|
112
|
+
if (entry === undefined)
|
|
113
|
+
return undefined;
|
|
114
|
+
return bucketOf(entry, bandForTime(time));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The peak-to-off-peak price multiplier for one model's entry, when the
|
|
118
|
+
* entry carries both bands. The multiplier is the ratio of the summed peak
|
|
119
|
+
* bucket rates to the summed off-peak bucket rates; official DeepSeek prices
|
|
120
|
+
* scale all buckets by the same factor, so this is a stable “peak is N× the
|
|
121
|
+
* off-peak price” display value.
|
|
122
|
+
* @param snapshot - the pricebook snapshot to read.
|
|
123
|
+
* @param provider - the provider route, when known.
|
|
124
|
+
* @param model - the model id.
|
|
125
|
+
* @returns the peak/off-peak multiplier, or null when either band is absent.
|
|
126
|
+
*/
|
|
127
|
+
export function peakOffPeakMultiplier(snapshot, provider, model) {
|
|
128
|
+
const entry = modelPriceOf(snapshot, provider, model);
|
|
129
|
+
if (entry === undefined)
|
|
130
|
+
return null;
|
|
131
|
+
const peak = entry.peak;
|
|
132
|
+
const offPeak = entry.offPeak;
|
|
133
|
+
if (peak === undefined || offPeak === undefined)
|
|
134
|
+
return null;
|
|
135
|
+
const offPeakTotal = offPeak.cacheReadPerMillion + offPeak.inputPerMillion + offPeak.outputPerMillion;
|
|
136
|
+
if (offPeakTotal === 0)
|
|
137
|
+
return null;
|
|
138
|
+
const peakTotal = peak.cacheReadPerMillion + peak.inputPerMillion + peak.outputPerMillion;
|
|
139
|
+
return peakTotal / offPeakTotal;
|
|
140
|
+
}
|
|
141
|
+
/** CJK characters (the /1.5 density class). */
|
|
142
|
+
const CJK_RE = /[\u3400-\u4dbf\u4e00-\u9fff]/g;
|
|
143
|
+
/**
|
|
144
|
+
* Heuristic character→token estimate: CJK ÷ 1.5, everything else ÷ 4
|
|
145
|
+
* (the streaming estimate's output side).
|
|
146
|
+
* @param text - the accumulated text.
|
|
147
|
+
* @returns the estimated token count.
|
|
148
|
+
*/
|
|
149
|
+
export function estimateTokens(text) {
|
|
150
|
+
const cjk = (text.match(CJK_RE) ?? []).length;
|
|
151
|
+
return Math.ceil(cjk / 1.5 + (text.length - cjk) / 4);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Tokens of the streamed text/reasoning blocks of a partial assistant.
|
|
155
|
+
* @param blocks - the partial's content blocks (text/reasoning carry `text`).
|
|
156
|
+
* @returns the estimated token count.
|
|
157
|
+
*/
|
|
158
|
+
export function blocksOutputTokens(blocks) {
|
|
159
|
+
let tokens = 0;
|
|
160
|
+
for (const block of blocks ?? []) {
|
|
161
|
+
if ((block.kind === 'text' || block.kind === 'reasoning') && typeof block.text === 'string' && block.text.length > 0) {
|
|
162
|
+
tokens += estimateTokens(block.text);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return tokens;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The cache-read ratio of the newest completed step (input side of the
|
|
169
|
+
* estimate). Absent history conservatively prices all input as uncached
|
|
170
|
+
* (upper bound).
|
|
171
|
+
* @param steps - the projection's step ledger.
|
|
172
|
+
* @returns the ratio in [0, 1].
|
|
173
|
+
*/
|
|
174
|
+
export function cacheReadRatioOf(steps) {
|
|
175
|
+
for (let i = steps.length - 1; i >= 0; i -= 1) {
|
|
176
|
+
const step = steps[i];
|
|
177
|
+
const total = step.uncachedInputTokens + step.cacheReadTokens + step.cacheWriteTokens;
|
|
178
|
+
if (step.priced && total > 0)
|
|
179
|
+
return step.cacheReadTokens / total;
|
|
180
|
+
}
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The live estimate of the running reply: input at the projected context
|
|
185
|
+
* size (weighted by the cache ratio) plus output at the streamed-token
|
|
186
|
+
* heuristic, both at CURRENT prices. Always labeled 估算/estimate — it is
|
|
187
|
+
* replaced by the projection's exact anchored value once the step settles.
|
|
188
|
+
* @param bucket - the current price bucket of the session model.
|
|
189
|
+
* @param inputTokens - the projected prompt size for the next request.
|
|
190
|
+
* @param cachedRatio - the cache-read ratio from recent completed steps.
|
|
191
|
+
* @param outputTokens - the streamed output tokens so far.
|
|
192
|
+
* @returns the estimated CNY cost.
|
|
193
|
+
*/
|
|
194
|
+
export function estimateCost(bucket, inputTokens, cachedRatio, outputTokens) {
|
|
195
|
+
const inputCost = inputTokens / 1_000_000
|
|
196
|
+
* (bucket.cacheReadPerMillion * cachedRatio + bucket.inputPerMillion * (1 - cachedRatio));
|
|
197
|
+
const outputCost = outputTokens / 1_000_000 * bucket.outputPerMillion;
|
|
198
|
+
return inputCost + outputCost;
|
|
199
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared display formatting for amounts and timestamps.
|
|
3
|
+
*
|
|
4
|
+
* @module @gamegeek-saikel/dsh-cost-meter/client/format
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Common currency symbols; unknown currencies render as a bare code.
|
|
8
|
+
* @param currency - ISO 4217 code.
|
|
9
|
+
* @returns the display symbol.
|
|
10
|
+
*/
|
|
11
|
+
export declare function currencySymbol(currency: string): string;
|
|
12
|
+
/** Detect the display currency from the active locale dictionary. */
|
|
13
|
+
export declare function displayCurrency(t: (key: string, params?: Record<string, string>) => string): 'CNY' | 'USD';
|
|
14
|
+
/**
|
|
15
|
+
* Compact money: two decimals above 1, four down to 0.01, six below.
|
|
16
|
+
* Costs are CNY 元 with 4-decimal display precision; zero renders as 0.00.
|
|
17
|
+
* @param value - the amount.
|
|
18
|
+
* @returns the fixed-width display string.
|
|
19
|
+
*/
|
|
20
|
+
export declare function formatMoney(value: number): string;
|
|
21
|
+
/** Time formatting for snapshot stamps and step rows. */
|
|
22
|
+
export declare function formatTime(epochMs: number): string;
|
|
23
|
+
/**
|
|
24
|
+
* Compact price multiplier, e.g. `2.0×` or `0.5×`.
|
|
25
|
+
* @param multiplier - the ratio to display.
|
|
26
|
+
* @returns the fixed one-decimal multiplier string.
|
|
27
|
+
*/
|
|
28
|
+
export declare function formatMultiplier(multiplier: number): string;
|
|
29
|
+
/**
|
|
30
|
+
* Compact percentage, e.g. `50%`.
|
|
31
|
+
* @param fraction - the ratio in [0, 1].
|
|
32
|
+
* @returns the whole-percent string.
|
|
33
|
+
*/
|
|
34
|
+
export declare function formatPercent(fraction: number): string;
|
|
35
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../../src/client/format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWvD;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,GAAG,KAAK,GAAG,KAAK,CAE1G;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED,yDAAyD;AACzD,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D;AACD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtD"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared display formatting for amounts and timestamps.
|
|
3
|
+
*
|
|
4
|
+
* @module @gamegeek-saikel/dsh-cost-meter/client/format
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Common currency symbols; unknown currencies render as a bare code.
|
|
8
|
+
* @param currency - ISO 4217 code.
|
|
9
|
+
* @returns the display symbol.
|
|
10
|
+
*/
|
|
11
|
+
export function currencySymbol(currency) {
|
|
12
|
+
switch (currency) {
|
|
13
|
+
case 'CNY':
|
|
14
|
+
return '¥';
|
|
15
|
+
case 'USD':
|
|
16
|
+
return '$';
|
|
17
|
+
case 'EUR':
|
|
18
|
+
return '€';
|
|
19
|
+
default:
|
|
20
|
+
return `${currency} `;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Detect the display currency from the active locale dictionary. */
|
|
24
|
+
export function displayCurrency(t) {
|
|
25
|
+
return t('view.cost') === 'Cost' ? 'USD' : 'CNY';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compact money: two decimals above 1, four down to 0.01, six below.
|
|
29
|
+
* Costs are CNY 元 with 4-decimal display precision; zero renders as 0.00.
|
|
30
|
+
* @param value - the amount.
|
|
31
|
+
* @returns the fixed-width display string.
|
|
32
|
+
*/
|
|
33
|
+
export function formatMoney(value) {
|
|
34
|
+
if (value === 0)
|
|
35
|
+
return '0.00';
|
|
36
|
+
if (value >= 1)
|
|
37
|
+
return value.toFixed(2);
|
|
38
|
+
if (value >= 0.01)
|
|
39
|
+
return value.toFixed(4);
|
|
40
|
+
return value.toFixed(6);
|
|
41
|
+
}
|
|
42
|
+
/** Time formatting for snapshot stamps and step rows. */
|
|
43
|
+
export function formatTime(epochMs) {
|
|
44
|
+
return new Date(epochMs).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Compact price multiplier, e.g. `2.0×` or `0.5×`.
|
|
48
|
+
* @param multiplier - the ratio to display.
|
|
49
|
+
* @returns the fixed one-decimal multiplier string.
|
|
50
|
+
*/
|
|
51
|
+
export function formatMultiplier(multiplier) {
|
|
52
|
+
return `${multiplier.toFixed(1)}×`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Compact percentage, e.g. `50%`.
|
|
56
|
+
* @param fraction - the ratio in [0, 1].
|
|
57
|
+
* @returns the whole-percent string.
|
|
58
|
+
*/
|
|
59
|
+
export function formatPercent(fraction) {
|
|
60
|
+
return `${Math.round(fraction * 100)}%`;
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost-meter plugin, browser half: contributes the cost surfaces —
|
|
3
|
+
* the composer-dock readout (anchored session spend + account balance), the
|
|
4
|
+
* Cost view tab, the per-reply cost chip, the header pill with the live
|
|
5
|
+
* streaming estimate, and the plugin configuration card (设置 → 插件). The
|
|
6
|
+
* anchored ledger arrives through the `sessionCost` projection; the balance
|
|
7
|
+
* and the pricebook arrive over the trust-fenced host `/cost-meter`
|
|
8
|
+
* route; the editable configuration binds the `cost-meter` settings
|
|
9
|
+
* namespace through the standard settings scope.
|
|
10
|
+
*/
|
|
11
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
12
|
+
import { type ConversationCostKey } from './locales.ts';
|
|
13
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
14
|
+
interface LocaleNamespaceMap {
|
|
15
|
+
/** Cost meter and account balance copy. */
|
|
16
|
+
'cost-meter': ConversationCostKey;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** The settings namespace this plugin owns (mirrors the host registration). */
|
|
20
|
+
export declare const SETTINGS_NAMESPACE = "cost-meter";
|
|
21
|
+
/** Required services for locale registration, slot contributions, and the settings scope. */
|
|
22
|
+
export declare const inject: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Client plugin body: register the dictionaries and the five slot entries.
|
|
25
|
+
* @param ctx - client root context.
|
|
26
|
+
*/
|
|
27
|
+
export declare function apply(ctx: ClientContext): void;
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAS3E,OAAO,EAAc,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAOnE,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,2CAA2C;QAC3C,YAAY,EAAE,mBAAmB,CAAA;KAClC;CACF;AAED,+EAA+E;AAC/E,eAAO,MAAM,kBAAkB,eAAe,CAAA;AAE9C,6FAA6F;AAC7F,eAAO,MAAM,MAAM,UAAmD,CAAA;AAEtE;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAgE9C"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { en, NS, zh } from "./locales.js";
|
|
2
|
+
import { SessionCostLine } from "./SessionCostLine.js";
|
|
3
|
+
import { CostView } from "./CostView.js";
|
|
4
|
+
import { AssistantCostChip } from "./AssistantCostChip.js";
|
|
5
|
+
import { SessionCostPill } from "./SessionCostPill.js";
|
|
6
|
+
import { CostPluginCard } from "./CostPluginCard.js";
|
|
7
|
+
/** The settings namespace this plugin owns (mirrors the host registration). */
|
|
8
|
+
export const SETTINGS_NAMESPACE = 'cost-meter';
|
|
9
|
+
/** Required services for locale registration, slot contributions, and the settings scope. */
|
|
10
|
+
export const inject = ['sessions', 'slots', 'locale', 'settingsScope'];
|
|
11
|
+
/**
|
|
12
|
+
* Client plugin body: register the dictionaries and the five slot entries.
|
|
13
|
+
* @param ctx - client root context.
|
|
14
|
+
*/
|
|
15
|
+
export function apply(ctx) {
|
|
16
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-cost-meter: dictionaries');
|
|
17
|
+
const t = ctx.locale.bind(NS);
|
|
18
|
+
const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
|
|
19
|
+
// Composer-dock readout: anchored session spend + balance (after the
|
|
20
|
+
// shipped stats line, order 0).
|
|
21
|
+
ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
|
|
22
|
+
name: 'conversation.composer.dock',
|
|
23
|
+
id: 'cost-meter',
|
|
24
|
+
order: 1,
|
|
25
|
+
locale: NS,
|
|
26
|
+
}, SessionCostLine));
|
|
27
|
+
// Cost view tab (after trajectory at order 10).
|
|
28
|
+
ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
29
|
+
name: 'conversation.view',
|
|
30
|
+
id: 'cost',
|
|
31
|
+
order: 20,
|
|
32
|
+
locale: NS,
|
|
33
|
+
label: () => t('view.cost'),
|
|
34
|
+
}, CostView));
|
|
35
|
+
// Per-reply cost chip (after the feedback action at order 10).
|
|
36
|
+
ctx.slots.inject('conversation.chat.assistant-actions', () => ctx.slots.register({
|
|
37
|
+
name: 'conversation.chat.assistant-actions',
|
|
38
|
+
id: 'cost',
|
|
39
|
+
order: 20,
|
|
40
|
+
locale: NS,
|
|
41
|
+
}, AssistantCostChip));
|
|
42
|
+
// Header pill: total / live estimate with the expandable detail.
|
|
43
|
+
// Negative order keeps it left of the built-in Session log utility.
|
|
44
|
+
ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
|
|
45
|
+
name: 'conversation.session.header.utilities',
|
|
46
|
+
id: 'cost-meter',
|
|
47
|
+
order: -10,
|
|
48
|
+
locale: NS,
|
|
49
|
+
}, SessionCostPill));
|
|
50
|
+
// Plugin configuration card (设置 → 插件), standard plugin-card shape over
|
|
51
|
+
// the `cost-meter` settings namespace.
|
|
52
|
+
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
|
|
53
|
+
name: 'settings.plugin.item',
|
|
54
|
+
id: 'cost-meter',
|
|
55
|
+
order: 30,
|
|
56
|
+
locale: NS,
|
|
57
|
+
inject: () => ({ scope }),
|
|
58
|
+
}, CostPluginCard));
|
|
59
|
+
}
|