@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
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/invariant.ts
|
|
2
|
+
const PACKAGE_NAME = "@gamegeek-saikel/dsh-cost-meter";
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
const name = "cost-meter-invariant";
|
|
5
|
+
/** Service required before the companion can register. */
|
|
6
|
+
const inject = ["invariants"];
|
|
7
|
+
/**
|
|
8
|
+
* Route-disposer symmetry: after the owning fiber of the /cost-meter
|
|
9
|
+
* route unloads, the webserver must no longer answer that path. The probe
|
|
10
|
+
* cycle registers and disposes twice on a reserved path — a leftover from
|
|
11
|
+
* the first cycle makes the second register throw the duplicate error,
|
|
12
|
+
* exactly the asymmetry this package would otherwise share with its host.
|
|
13
|
+
*/
|
|
14
|
+
const install = (ctx, fail) => {
|
|
15
|
+
ctx.on("internal/plugin", () => {
|
|
16
|
+
const server = ctx.get("webServer");
|
|
17
|
+
if (server === void 0) return;
|
|
18
|
+
const probe = {
|
|
19
|
+
kind: "exact",
|
|
20
|
+
path: "/__dsh_fare_meter_invariant_probe__",
|
|
21
|
+
handler: () => {}
|
|
22
|
+
};
|
|
23
|
+
try {
|
|
24
|
+
server.register(probe)();
|
|
25
|
+
server.register(probe)();
|
|
26
|
+
} catch {
|
|
27
|
+
fail("cost-meter webserver route disposer left a route registered — route tables and fiber lifecycles diverged");
|
|
28
|
+
}
|
|
29
|
+
}, { global: true });
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Register this package's invariant companion.
|
|
33
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
34
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
35
|
+
*/
|
|
36
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
37
|
+
//#endregion
|
|
38
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { UseProjection } from '@deepseek-ai/dsh-client-runtime/client';
|
|
2
|
+
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
+
import type { ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client';
|
|
4
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
|
+
import type { MessageId } from '@deepseek-ai/dsh-llm/brand';
|
|
6
|
+
import type { SessionCostStep } from '../types.ts';
|
|
7
|
+
export type ChipLocale = PropsLocale<'cost-meter'>['t'];
|
|
8
|
+
export interface AssistantCostChipProps {
|
|
9
|
+
/** Stable identity of the finalized assistant message the actions address. */
|
|
10
|
+
messageId: MessageId;
|
|
11
|
+
useSession: SnapshotSelectorHook<ConversationSnapshot>;
|
|
12
|
+
useProjection: UseProjection;
|
|
13
|
+
t: ChipLocale;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Locate a finalized assistant node by its message id and return its
|
|
17
|
+
* (turn, step) coordinates, or null when the node is out of window.
|
|
18
|
+
* @param snapshot - the conversation snapshot.
|
|
19
|
+
* @param messageId - the addressed message identity.
|
|
20
|
+
* @returns the step coordinates, or null.
|
|
21
|
+
*/
|
|
22
|
+
export declare function stepOfMessage(snapshot: ConversationSnapshot, messageId: MessageId): {
|
|
23
|
+
turn: number;
|
|
24
|
+
step: number;
|
|
25
|
+
} | null;
|
|
26
|
+
/** The projection step for one (turn, step) coordinate, if the ledger has it. */
|
|
27
|
+
export declare function stepOf(steps: readonly SessionCostStep[] | undefined, turn: number, step: number): SessionCostStep | undefined;
|
|
28
|
+
export declare const AssistantCostChip: import("react").MemoExoticComponent<({ messageId, useSession, useProjection, t }: AssistantCostChipProps) => import("react").JSX.Element | null>;
|
|
29
|
+
//# sourceMappingURL=AssistantCostChip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AssistantCostChip.d.ts","sourceRoot":"","sources":["../../../src/client/AssistantCostChip.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAC3E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAA;AAC5E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAA;AAClF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAC3D,OAAO,KAAK,EAA4B,eAAe,EAAE,MAAM,aAAa,CAAA;AAK5E,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,CAAA;AAEvD,MAAM,WAAW,sBAAsB;IACrC,8EAA8E;IAC9E,SAAS,EAAE,SAAS,CAAA;IACpB,UAAU,EAAE,oBAAoB,CAAC,oBAAoB,CAAC,CAAA;IACtD,aAAa,EAAE,aAAa,CAAA;IAC5B,CAAC,EAAE,UAAU,CAAA;CACd;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,oBAAoB,EAC9B,SAAS,EAAE,SAAS,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CASvC;AAED,iFAAiF;AACjF,wBAAgB,MAAM,CACpB,KAAK,EAAE,SAAS,eAAe,EAAE,GAAG,SAAS,EAC7C,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,eAAe,GAAG,SAAS,CAE7B;AAED,eAAO,MAAM,iBAAiB,oFAAgF,sBAAsB,wCA8ClI,CAAA"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Per-reply cost chip: the anchored cost of ONE finalized reply. Rendered
|
|
4
|
+
* through the assistant-actions slot but visually joined to the reply's
|
|
5
|
+
* timing row (01:51 · 用时 · 首 token · tok/s): a flex `order` places it
|
|
6
|
+
* after the clock text, and the type matches the clock's (label-tertiary,
|
|
7
|
+
* 14px) with a dot separator, so it reads as part of the same metadata run.
|
|
8
|
+
* The `messageId` owner prop maps to the assistant node's (turn, step),
|
|
9
|
+
* which addresses the projection's step ledger — the cost is the anchored
|
|
10
|
+
* snapshot value, never a current-price recompute. Unpriced replies render
|
|
11
|
+
* `—` (the Cost tab explains why).
|
|
12
|
+
*/
|
|
13
|
+
import { memo, useEffect, useState } from 'react';
|
|
14
|
+
import { bandForTime, peakOffPeakMultiplier } from "./cost-math.js";
|
|
15
|
+
import { currencySymbol, displayCurrency, formatMoney, formatMultiplier } from "./format.js";
|
|
16
|
+
import css from './AssistantCostChip.module.css';
|
|
17
|
+
/**
|
|
18
|
+
* Locate a finalized assistant node by its message id and return its
|
|
19
|
+
* (turn, step) coordinates, or null when the node is out of window.
|
|
20
|
+
* @param snapshot - the conversation snapshot.
|
|
21
|
+
* @param messageId - the addressed message identity.
|
|
22
|
+
* @returns the step coordinates, or null.
|
|
23
|
+
*/
|
|
24
|
+
export function stepOfMessage(snapshot, messageId) {
|
|
25
|
+
const nodes = snapshot.chat.legacy.nodes;
|
|
26
|
+
for (let i = nodes.length - 1; i >= 0; i -= 1) {
|
|
27
|
+
const node = nodes[i];
|
|
28
|
+
if (node?.kind === 'assistant' && node.messageId === messageId && node.turn !== undefined) {
|
|
29
|
+
return { turn: node.turn, step: node.step };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** The projection step for one (turn, step) coordinate, if the ledger has it. */
|
|
35
|
+
export function stepOf(steps, turn, step) {
|
|
36
|
+
return steps?.find(entry => entry.turn === turn && entry.step === step);
|
|
37
|
+
}
|
|
38
|
+
export const AssistantCostChip = memo(function AssistantCostChip({ messageId, useSession, useProjection, t }) {
|
|
39
|
+
const currency = displayCurrency(t);
|
|
40
|
+
const projectionKey = currency === 'USD' ? 'sessionCostUsd' : 'sessionCost';
|
|
41
|
+
const step = useSession(snapshot => stepOfMessage(snapshot, messageId));
|
|
42
|
+
const cost = useProjection(projectionKey);
|
|
43
|
+
const ledger = step === null ? undefined : stepOf(cost?.steps, step.turn, step.step);
|
|
44
|
+
const [response, setResponse] = useState(null);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
let alive = true;
|
|
47
|
+
void fetch(`/cost-meter?currency=${currency}`, { cache: 'no-store' })
|
|
48
|
+
.then(res => (res.ok ? res.json() : null))
|
|
49
|
+
.then((data) => {
|
|
50
|
+
if (!alive || data === null)
|
|
51
|
+
return;
|
|
52
|
+
setResponse(data);
|
|
53
|
+
})
|
|
54
|
+
.catch(() => { });
|
|
55
|
+
return () => {
|
|
56
|
+
alive = false;
|
|
57
|
+
};
|
|
58
|
+
}, [currency]);
|
|
59
|
+
if (ledger === undefined || ledger.cost === null) {
|
|
60
|
+
if (ledger === undefined)
|
|
61
|
+
return null;
|
|
62
|
+
// Unpriced reply: show the dash; the native title explains why.
|
|
63
|
+
const reason = ledger.unpricedReason === 'NO_MODEL' ? t('reason.NO_MODEL') : t('reason.NO_PRICE');
|
|
64
|
+
return (_jsx("span", { className: css.unpriced, "data-cost-chip": true, title: reason, "data-testid": "cost-chip-unpriced", children: t('chip.unpriced') }));
|
|
65
|
+
}
|
|
66
|
+
const amount = `${currencySymbol(currency)}${formatMoney(ledger.cost)}`;
|
|
67
|
+
const band = bandForTime(Date.now());
|
|
68
|
+
const ratio = peakOffPeakMultiplier(response?.pricebook?.current ?? null, ledger.provider, ledger.model);
|
|
69
|
+
const bandLabel = band === 'peak'
|
|
70
|
+
? ratio === null ? t('band.peak') : t('price.peakRatio', { multiplier: formatMultiplier(ratio) })
|
|
71
|
+
: band === 'offPeak'
|
|
72
|
+
? ratio === null ? t('band.offPeak') : t('price.offPeakRatio', { multiplier: formatMultiplier(1 / ratio) })
|
|
73
|
+
: null;
|
|
74
|
+
return (_jsxs("span", { className: css.root, "data-cost-chip": true, title: t('chip.title', { amount }), "data-testid": "cost-chip", children: [amount, bandLabel !== null && _jsx("span", { className: css.bandBadge, "data-band": band, "data-testid": "cost-chip-band", children: bandLabel })] }));
|
|
75
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
import type { SettingsScope } from '@deepseek-ai/dsh-client-runtime/client';
|
|
3
|
+
export type PluginCardLocale = PropsLocale<'cost-meter'>['t'];
|
|
4
|
+
export interface CostPluginCardProps {
|
|
5
|
+
/** The bound settings scope for the `cost-meter` namespace. */
|
|
6
|
+
scope: SettingsScope<unknown>;
|
|
7
|
+
t: PluginCardLocale;
|
|
8
|
+
}
|
|
9
|
+
export declare const CostPluginCard: import("react").MemoExoticComponent<({ scope, t }: CostPluginCardProps) => import("react").JSX.Element>;
|
|
10
|
+
//# sourceMappingURL=CostPluginCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CostPluginCard.d.ts","sourceRoot":"","sources":["../../../src/client/CostPluginCard.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAyB,MAAM,wCAAwC,CAAA;AAKlG,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,CAAA;AAE7D,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC7B,CAAC,EAAE,gBAAgB,CAAA;CACpB;AAyBD,eAAO,MAAM,cAAc,qDAA8C,mBAAmB,iCAsN1F,CAAA"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Plugin configuration card (设置 → 插件 → cost-meter): the standard
|
|
4
|
+
* DSH plugin-card shape (collapsible header, disclosure body, save/discard
|
|
5
|
+
* footer) over the `cost-meter` settings namespace. Every editable
|
|
6
|
+
* field binds the settings scope (`ctx.settingsScope.bind`) — the same
|
|
7
|
+
* transport the shipped llm-deepseek / bash / web-search cards use — so the
|
|
8
|
+
* card looks, stages, and persists like every other plugin configuration.
|
|
9
|
+
* The JSON textareas carry the two structured fields (per-model price
|
|
10
|
+
* overrides, OpenRouter aliases); the refresh button is a plain action.
|
|
11
|
+
*/
|
|
12
|
+
import { memo, useEffect, useState, useSyncExternalStore } from 'react';
|
|
13
|
+
import { Button, IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
14
|
+
import { formatTime } from "./format.js";
|
|
15
|
+
import css from './CostPluginCard.module.css';
|
|
16
|
+
const json = (value) => JSON.stringify(value ?? {}, null, 2);
|
|
17
|
+
export const CostPluginCard = memo(function CostPluginCard({ scope, t }) {
|
|
18
|
+
const snapshot = useSyncExternalStore((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
19
|
+
const [open, setOpen] = useState(false);
|
|
20
|
+
const [busy, setBusy] = useState(false);
|
|
21
|
+
const [status, setStatus] = useState(null);
|
|
22
|
+
const [error, setError] = useState(null);
|
|
23
|
+
const [staged, setStaged] = useState(null);
|
|
24
|
+
const [response, setResponse] = useState(null);
|
|
25
|
+
// Seed the staged form from the latest accepted section.
|
|
26
|
+
const value = snapshot.value;
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
setStaged({
|
|
29
|
+
overridesText: json(value?.overrides),
|
|
30
|
+
aliasesText: json(value?.aliases),
|
|
31
|
+
cacheReadDiscount: String(value?.cacheReadDiscount ?? 0.25),
|
|
32
|
+
fxMode: value?.fxMode ?? 'auto',
|
|
33
|
+
manualRate: String(value?.manualRate ?? 7.2),
|
|
34
|
+
balanceEnabled: value?.balanceEnabled ?? true,
|
|
35
|
+
openRouterEnabled: value?.openRouterEnabled ?? true,
|
|
36
|
+
});
|
|
37
|
+
}, [value]);
|
|
38
|
+
// Snapshot info for the header hint (source/time, no version numbers).
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let alive = true;
|
|
41
|
+
const load = () => {
|
|
42
|
+
void fetch('/cost-meter', { cache: 'no-store' })
|
|
43
|
+
.then(res => (res.ok ? res.json() : null))
|
|
44
|
+
.then((data) => {
|
|
45
|
+
if (!alive || data === null)
|
|
46
|
+
return;
|
|
47
|
+
setResponse(data);
|
|
48
|
+
})
|
|
49
|
+
.catch(() => { });
|
|
50
|
+
};
|
|
51
|
+
load();
|
|
52
|
+
return () => {
|
|
53
|
+
alive = false;
|
|
54
|
+
};
|
|
55
|
+
}, []);
|
|
56
|
+
const current = response?.pricebook.current ?? null;
|
|
57
|
+
const source = current === null
|
|
58
|
+
? t('source.none')
|
|
59
|
+
: t(`source.${current.source}`);
|
|
60
|
+
const write = async (field, fieldValue) => {
|
|
61
|
+
setBusy(true);
|
|
62
|
+
setStatus(null);
|
|
63
|
+
setError(null);
|
|
64
|
+
try {
|
|
65
|
+
await scope.set(field, fieldValue);
|
|
66
|
+
setStatus(t('settings.saved'));
|
|
67
|
+
}
|
|
68
|
+
catch (cause) {
|
|
69
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
setBusy(false);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const save = () => {
|
|
76
|
+
if (staged === null)
|
|
77
|
+
return;
|
|
78
|
+
setError(null);
|
|
79
|
+
let overrides;
|
|
80
|
+
let aliases;
|
|
81
|
+
try {
|
|
82
|
+
overrides = JSON.parse(staged.overridesText);
|
|
83
|
+
aliases = JSON.parse(staged.aliasesText);
|
|
84
|
+
}
|
|
85
|
+
catch (cause) {
|
|
86
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
void write('overrides', overrides);
|
|
90
|
+
void write('aliases', aliases);
|
|
91
|
+
const discount = Number(staged.cacheReadDiscount);
|
|
92
|
+
if (Number.isFinite(discount) && discount >= 0 && discount <= 1)
|
|
93
|
+
void write('cacheReadDiscount', discount);
|
|
94
|
+
const rate = Number(staged.manualRate);
|
|
95
|
+
if (Number.isFinite(rate) && rate > 0)
|
|
96
|
+
void write('manualRate', rate);
|
|
97
|
+
if (staged.fxMode === 'auto' || staged.fxMode === 'manual')
|
|
98
|
+
void write('fxMode', staged.fxMode);
|
|
99
|
+
void write('balanceEnabled', staged.balanceEnabled);
|
|
100
|
+
void write('openRouterEnabled', staged.openRouterEnabled);
|
|
101
|
+
};
|
|
102
|
+
const refresh = () => {
|
|
103
|
+
setBusy(true);
|
|
104
|
+
setStatus(null);
|
|
105
|
+
setError(null);
|
|
106
|
+
void fetch('/cost-meter', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: { 'content-type': 'application/json' },
|
|
109
|
+
body: JSON.stringify({ action: 'refresh' }),
|
|
110
|
+
})
|
|
111
|
+
.then(res => (res.ok ? res.json() : null))
|
|
112
|
+
.then((data) => {
|
|
113
|
+
if (data !== null)
|
|
114
|
+
setResponse(data);
|
|
115
|
+
setStatus(t('settings.saved'));
|
|
116
|
+
})
|
|
117
|
+
.catch((cause) => setError(cause instanceof Error ? cause.message : String(cause)))
|
|
118
|
+
.finally(() => setBusy(false));
|
|
119
|
+
};
|
|
120
|
+
const hint = current === null
|
|
121
|
+
? t('settings.noSnapshot')
|
|
122
|
+
: `${t('settings.current', { source, time: formatTime(current.effectiveAt) })}`;
|
|
123
|
+
return (_jsxs("li", { className: css.card + (open ? ` ${css.cardOpen}` : ''), "data-testid": "cost-plugin-card", children: [_jsxs("button", { type: "button", className: css.header, onClick: () => setOpen(prev => !prev), "aria-expanded": open, children: [_jsxs("span", { className: css.headText, children: [_jsx("span", { className: css.name, children: t('settings.title') }), _jsx("span", { className: css.description, children: hint })] }), error !== null && _jsx("span", { className: css.pending, children: t('settings.error', { error }) }), _jsx(IconChevronDownOutline14, { className: css.chevron + (open ? ` ${css.chevronOpen}` : '') })] }), open && (_jsxs("div", { className: css.body, children: [_jsxs("div", { className: css.field, children: [_jsx("div", { className: css.head, children: _jsx("span", { className: css.label, children: t('settings.discount') }) }), _jsx("input", { className: css.input, type: "number", min: 0, max: 1, step: 0.01, value: staged?.cacheReadDiscount ?? '', onChange: event => setStaged(prev => prev === null ? prev : { ...prev, cacheReadDiscount: event.target.value }) })] }), _jsxs("div", { className: css.field, children: [_jsx("div", { className: css.head, children: _jsx("span", { className: css.label, children: t('settings.fx') }) }), _jsxs("div", { className: css.inline, children: [_jsxs("select", { className: css.select, value: staged?.fxMode ?? 'auto', onChange: event => setStaged(prev => prev === null ? prev : { ...prev, fxMode: event.target.value }), children: [_jsx("option", { value: "auto", children: t('settings.fx.auto') }), _jsx("option", { value: "manual", children: t('settings.fx.manual') })] }), _jsx("input", { className: css.input, type: "number", min: 0.001, step: 0.01, value: staged?.manualRate ?? '', onChange: event => setStaged(prev => prev === null ? prev : { ...prev, manualRate: event.target.value }) })] })] }), _jsx("label", { className: css.field, children: _jsxs("div", { className: css.head, children: [_jsx("span", { className: css.label, children: t('settings.balance') }), _jsx("input", { className: css.checkbox, type: "checkbox", checked: staged?.balanceEnabled ?? true, onChange: event => setStaged(prev => prev === null ? prev : { ...prev, balanceEnabled: event.target.checked }) })] }) }), _jsx("label", { className: css.field, children: _jsxs("div", { className: css.head, children: [_jsx("span", { className: css.label, children: t('settings.openrouter') }), _jsx("input", { className: css.checkbox, type: "checkbox", checked: staged?.openRouterEnabled ?? true, onChange: event => setStaged(prev => prev === null ? prev : { ...prev, openRouterEnabled: event.target.checked }) })] }) }), _jsxs("div", { className: css.field, children: [_jsx("div", { className: css.head, children: _jsx("span", { className: css.label, children: t('settings.overrides') }) }), _jsx("textarea", { className: css.textarea, value: staged?.overridesText ?? '', spellCheck: false, onChange: event => setStaged(prev => prev === null ? prev : { ...prev, overridesText: event.target.value }) }), _jsx("div", { className: css.hint, children: t('settings.jsonHint') })] }), _jsxs("div", { className: css.field, children: [_jsx("div", { className: css.head, children: _jsx("span", { className: css.label, children: t('settings.aliases') }) }), _jsx("textarea", { className: css.textarea, value: staged?.aliasesText ?? '', spellCheck: false, onChange: event => setStaged(prev => prev === null ? prev : { ...prev, aliasesText: event.target.value }) })] }), _jsxs("div", { className: css.footer, children: [status !== null && _jsx("span", { className: css.status, children: status }), _jsx("span", { className: css.spacer }), _jsx(Button, { variant: "outline", onClick: () => setStaged(null), children: t('settings.discard') }), _jsx(Button, { variant: "primary", onClick: save, disabled: busy || snapshot.status !== 'ready', children: t('settings.save') }), _jsx(Button, { variant: "outline", onClick: refresh, disabled: busy, children: busy ? t('settings.refreshing') : t('settings.refresh') })] })] }))] }));
|
|
124
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { UseProjection } from '@deepseek-ai/dsh-client-runtime/client';
|
|
2
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
+
export type CostViewLocale = PropsLocale<'cost-meter'>['t'];
|
|
4
|
+
export interface CostViewProps {
|
|
5
|
+
useProjection: UseProjection;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
t: CostViewLocale;
|
|
8
|
+
}
|
|
9
|
+
export declare const CostView: import("react").MemoExoticComponent<({ useProjection, sessionId, t }: CostViewProps) => import("react").JSX.Element>;
|
|
10
|
+
//# sourceMappingURL=CostView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CostView.d.ts","sourceRoot":"","sources":["../../../src/client/CostView.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAC3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAMnE,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,CAAA;AAE3D,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,aAAa,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,CAAC,EAAE,cAAc,CAAA;CAClB;AAOD,eAAO,MAAM,QAAQ,wEAA2D,aAAa,iCAkH3F,CAAA"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Cost view tab: the whole conversation's anchored cost ledger — the total
|
|
4
|
+
* (main session + subagent sessions, host-aggregated), the three billed
|
|
5
|
+
* categories (uncached input incl. cache writes, cache reads, output), a
|
|
6
|
+
* per-subagent breakdown, and the per-reply list (turn, time, model, token
|
|
7
|
+
* split, step cost, anchored snapshot version and effective time). Reads the
|
|
8
|
+
* `sessionCost` projection plus the host-aggregated subagent totals; nothing
|
|
9
|
+
* is recomputed at current prices.
|
|
10
|
+
*/
|
|
11
|
+
import { memo, useEffect, useState } from 'react';
|
|
12
|
+
import { bandForTime, combineTotals, peakOffPeakMultiplier, subagentSpend } from "./cost-math.js";
|
|
13
|
+
import { currencySymbol, displayCurrency, formatMoney, formatMultiplier, formatTime } from "./format.js";
|
|
14
|
+
import css from './CostView.module.css';
|
|
15
|
+
/** Map an unpriced reason to its localized copy. */
|
|
16
|
+
function reasonText(step, t) {
|
|
17
|
+
return t(`reason.${step.unpricedReason ?? 'NO_PRICE'}`);
|
|
18
|
+
}
|
|
19
|
+
export const CostView = memo(function CostView({ useProjection, sessionId, t }) {
|
|
20
|
+
const currency = displayCurrency(t);
|
|
21
|
+
const projectionKey = currency === 'USD' ? 'sessionCostUsd' : 'sessionCost';
|
|
22
|
+
const cost = useProjection(projectionKey);
|
|
23
|
+
const [response, setResponse] = useState(null);
|
|
24
|
+
const symbol = currencySymbol(currency);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
let alive = true;
|
|
27
|
+
void fetch(`/cost-meter?session=${encodeURIComponent(sessionId)}¤cy=${currency}`, { cache: 'no-store' })
|
|
28
|
+
.then(res => (res.ok ? res.json() : null))
|
|
29
|
+
.then((data) => {
|
|
30
|
+
if (!alive || data === null)
|
|
31
|
+
return;
|
|
32
|
+
setResponse(data);
|
|
33
|
+
})
|
|
34
|
+
.catch(() => { });
|
|
35
|
+
return () => {
|
|
36
|
+
alive = false;
|
|
37
|
+
};
|
|
38
|
+
}, [sessionId, currency]);
|
|
39
|
+
if (cost === undefined || cost.steps.length === 0) {
|
|
40
|
+
return _jsx("div", { className: css.root, "data-testid": "cost-view-empty", children: t('view.empty') });
|
|
41
|
+
}
|
|
42
|
+
const rows = cost.steps;
|
|
43
|
+
const combined = combineTotals(cost.totals, response?.subagents);
|
|
44
|
+
const totals = combined ?? cost.totals;
|
|
45
|
+
const subSpend = subagentSpend(response?.subagents);
|
|
46
|
+
const money = (value) => value === null ? '—' : `${symbol}${formatMoney(value)}`;
|
|
47
|
+
return (_jsxs("div", { className: css.root, "data-testid": "cost-view", children: [_jsxs("section", { className: css.summary, children: [_jsxs("div", { className: css.totalRow, children: [_jsx("span", { className: css.totalLabel, children: t('view.total') }), _jsx("span", { className: css.totalValue, children: money(totals.cost) })] }), _jsxs("div", { className: css.categories, children: [_jsxs("span", { children: [t('view.uncached'), ": ", money(totals.uncachedCost)] }), _jsxs("span", { children: [t('view.cacheRead'), ": ", money(totals.cacheReadCost)] }), _jsxs("span", { children: [t('view.output'), ": ", money(totals.outputCost)] })] }), totals.unpricedSteps > 0 && (_jsx("div", { className: css.unpricedNote, children: t('view.unpriced', { count: String(totals.unpricedSteps) }) })), response !== null && response.subagents.length > 0 && (_jsxs("section", { className: css.subagents, "data-testid": "cost-view-subagents", children: [_jsx("div", { className: css.unpricedNote, children: t('line.subagent', { amount: `${symbol}${formatMoney(subSpend)}` }) }), _jsx("ol", { className: css.stepList, children: response.subagents.map(sub => (_jsxs("li", { className: css.step, children: [_jsxs("div", { className: css.stepHead, children: [_jsx("span", { className: css.stepTurn, children: sub.sessionId.slice(0, 8) }), _jsx("span", { className: css.stepCost, children: money(sub.totals.cost) })] }), _jsxs("div", { className: css.stepBody, children: [_jsxs("span", { children: [t('view.uncached'), ": ", money(sub.totals.uncachedCost)] }), _jsxs("span", { children: [t('view.cacheRead'), ": ", money(sub.totals.cacheReadCost)] }), _jsxs("span", { children: [t('view.output'), ": ", money(sub.totals.outputCost)] })] })] }, sub.sessionId))) })] }))] }), _jsxs("section", { className: css.steps, children: [_jsx("h3", { className: css.stepsTitle, children: t('view.steps') }), _jsx("ol", { className: css.stepList, children: rows.map((step) => {
|
|
48
|
+
const stepBand = bandForTime(Date.now());
|
|
49
|
+
const stepRatio = peakOffPeakMultiplier(response?.pricebook?.current ?? null, step.provider, step.model);
|
|
50
|
+
const stepBandLabel = stepBand === 'peak'
|
|
51
|
+
? stepRatio === null ? t('band.peak') : t('price.peakRatio', { multiplier: formatMultiplier(stepRatio) })
|
|
52
|
+
: stepBand === 'offPeak'
|
|
53
|
+
? stepRatio === null ? t('band.offPeak') : t('price.offPeakRatio', { multiplier: formatMultiplier(1 / stepRatio) })
|
|
54
|
+
: null;
|
|
55
|
+
return (_jsxs("li", { className: css.step, "data-testid": "cost-step", "data-band": stepBand === 'single' ? undefined : stepBand, children: [_jsxs("div", { className: css.stepHead, children: [_jsx("span", { className: css.stepTurn, children: t('step.turn', { turn: String(step.turn) }) }), _jsx("span", { className: css.stepModel, children: t('step.model', { model: step.model }) }), _jsx("span", { className: css.stepTime, children: formatTime(step.time) }), _jsxs("span", { className: css.stepCost, children: [money(step.cost), stepBandLabel !== null && _jsx("span", { className: css.bandBadge, "data-band": stepBand, "data-testid": "cost-step-band", children: stepBandLabel })] })] }), _jsxs("div", { className: css.stepBody, children: [_jsx("span", { className: css.stepTokens, children: t('step.tokens', {
|
|
56
|
+
uncached: String(step.uncachedInputTokens),
|
|
57
|
+
read: String(step.cacheReadTokens),
|
|
58
|
+
write: String(step.cacheWriteTokens),
|
|
59
|
+
output: String(step.outputTokens),
|
|
60
|
+
}) }), _jsx("span", { className: css.stepMeta, children: step.priced && step.snapshotVersion !== null
|
|
61
|
+
? t('step.snapshot', {
|
|
62
|
+
time: formatTime(step.snapshotEffectiveAt ?? step.time),
|
|
63
|
+
})
|
|
64
|
+
: t('step.unpriced', { reason: reasonText(step, t) }) })] })] }, `${step.turn}:${step.step}`));
|
|
65
|
+
}) })] })] }));
|
|
66
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { UseProjection } from '@deepseek-ai/dsh-client-runtime/client';
|
|
2
|
+
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
+
import type { ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client';
|
|
4
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
|
+
import type { SessionCostProjection } from '../types.ts';
|
|
6
|
+
/** Host route serving the balance snapshot and the pricebook view. */
|
|
7
|
+
export declare const ENDPOINT = "/cost-meter";
|
|
8
|
+
/** Balance refresh interval in milliseconds. */
|
|
9
|
+
export declare const REFRESH_MS = 60000;
|
|
10
|
+
/** Session-cost namespace key domain for the locale seat. */
|
|
11
|
+
export type CostLocale = PropsLocale<'cost-meter'>['t'];
|
|
12
|
+
export interface SessionCostLineProps {
|
|
13
|
+
useSession: SnapshotSelectorHook<ConversationSnapshot>;
|
|
14
|
+
useProjection: UseProjection;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
t: CostLocale;
|
|
17
|
+
}
|
|
18
|
+
/** The tooltip's cost detail rows from the anchored projection. Never throws. */
|
|
19
|
+
export declare function costDetail(cost: SessionCostProjection | undefined, model: string | undefined, t: CostLocale, currency?: 'CNY' | 'USD'): string[];
|
|
20
|
+
export declare const SessionCostLine: import("react").MemoExoticComponent<({ useProjection, sessionId, t }: SessionCostLineProps) => import("react").JSX.Element | null>;
|
|
21
|
+
//# sourceMappingURL=SessionCostLine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionCostLine.d.ts","sourceRoot":"","sources":["../../../src/client/SessionCostLine.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAC3E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAA;AAC5E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAA;AAClF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAA6C,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAKnG,sEAAsE;AACtE,eAAO,MAAM,QAAQ,gBAAgB,CAAA;AACrC,gDAAgD;AAChD,eAAO,MAAM,UAAU,QAAS,CAAA;AAEhC,6DAA6D;AAC7D,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,CAAA;AAEvD,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,oBAAoB,CAAC,oBAAoB,CAAC,CAAA;IACtD,aAAa,EAAE,aAAa,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,CAAC,EAAE,UAAU,CAAA;CACd;AAaD,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,EAAE,qBAAqB,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,QAAQ,GAAE,KAAK,GAAG,KAAa,GAAG,MAAM,EAAE,CA2BvJ;AAED,eAAO,MAAM,eAAe,wEAAkE,oBAAoB,wCA0GhH,CAAA"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Composer-dock readout: the current session's anchored spend (the
|
|
4
|
+
* `sessionCost` projection — price-snapshot-anchored, never recomputed at
|
|
5
|
+
* current prices) plus the DeepSeek account balance (host
|
|
6
|
+
* `/cost-meter` route), refreshed every minute. The hover detail
|
|
7
|
+
* breaks the spend into the three billed categories (uncached input incl.
|
|
8
|
+
* cache writes, cache reads, output) and names the anchored price snapshot.
|
|
9
|
+
*
|
|
10
|
+
* The component is deliberately defensive: every derived string is computed
|
|
11
|
+
* inside a guarded block so a single bad field can never take down the whole
|
|
12
|
+
* dock entry (the slot system retires crashed entries).
|
|
13
|
+
*/
|
|
14
|
+
import { memo, useEffect, useMemo, useState } from 'react';
|
|
15
|
+
import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
16
|
+
import { combineTotals, subagentSpend } from "./cost-math.js";
|
|
17
|
+
import { currencySymbol, displayCurrency, formatMoney, formatTime } from "./format.js";
|
|
18
|
+
import css from './SessionCostLine.module.css';
|
|
19
|
+
/** Host route serving the balance snapshot and the pricebook view. */
|
|
20
|
+
export const ENDPOINT = '/cost-meter';
|
|
21
|
+
/** Balance refresh interval in milliseconds. */
|
|
22
|
+
export const REFRESH_MS = 60_000;
|
|
23
|
+
/** Compose the tooltip's balance detail from an ok snapshot. */
|
|
24
|
+
function balanceTooltip(balance, t) {
|
|
25
|
+
if (balance.balance === null)
|
|
26
|
+
return t('balance.failed');
|
|
27
|
+
const symbol = currencySymbol(balance.balance.currency);
|
|
28
|
+
const amount = `${symbol}${formatMoney(balance.balance.total)}`;
|
|
29
|
+
const granted = `${symbol}${formatMoney(balance.balance.granted)}`;
|
|
30
|
+
const toppedUp = `${symbol}${formatMoney(balance.balance.toppedUp)}`;
|
|
31
|
+
const detail = t('balance.detail', { amount, granted, toppedUp });
|
|
32
|
+
return balance.isAvailable ? detail : `${detail} · ${t('balance.suspended')}`;
|
|
33
|
+
}
|
|
34
|
+
/** The tooltip's cost detail rows from the anchored projection. Never throws. */
|
|
35
|
+
export function costDetail(cost, model, t, currency = 'CNY') {
|
|
36
|
+
const rows = [];
|
|
37
|
+
try {
|
|
38
|
+
if (cost === undefined)
|
|
39
|
+
return rows;
|
|
40
|
+
const symbol = currencySymbol(currency);
|
|
41
|
+
const totals = cost.totals;
|
|
42
|
+
if (totals !== undefined && totals.pricedSteps > 0) {
|
|
43
|
+
rows.push(t('cost.label', { amount: `${symbol}${formatMoney(totals.cost)}` }));
|
|
44
|
+
rows.push(t('view.uncached') + ` ${symbol}${formatMoney(totals.uncachedCost)}`);
|
|
45
|
+
rows.push(t('view.cacheRead') + ` ${symbol}${formatMoney(totals.cacheReadCost)}`);
|
|
46
|
+
rows.push(t('view.output') + ` ${symbol}${formatMoney(totals.outputCost)}`);
|
|
47
|
+
}
|
|
48
|
+
if (model !== undefined)
|
|
49
|
+
rows.push(t('step.model', { model }));
|
|
50
|
+
const steps = cost.steps;
|
|
51
|
+
if (steps !== undefined && steps.length > 0) {
|
|
52
|
+
const last = steps[steps.length - 1];
|
|
53
|
+
if (last !== undefined && last.snapshotVersion !== null) {
|
|
54
|
+
rows.push(t('pill.detail.snapshot', { time: formatTime(last.snapshotEffectiveAt ?? last.time) }));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (totals !== undefined && totals.unpricedSteps > 0) {
|
|
58
|
+
rows.push(t('view.unpriced', { count: String(totals.unpricedSteps) }));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* a malformed field must never take down the dock entry */
|
|
63
|
+
}
|
|
64
|
+
return rows;
|
|
65
|
+
}
|
|
66
|
+
export const SessionCostLine = memo(function SessionCostLine({ useProjection, sessionId, t }) {
|
|
67
|
+
const currency = displayCurrency(t);
|
|
68
|
+
const projectionKey = currency === 'USD' ? 'sessionCostUsd' : 'sessionCost';
|
|
69
|
+
const cost = useProjection(projectionKey);
|
|
70
|
+
const [response, setResponse] = useState(null);
|
|
71
|
+
const [failed, setFailed] = useState(false);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
let alive = true;
|
|
74
|
+
let timer;
|
|
75
|
+
const endpoint = `${ENDPOINT}?session=${encodeURIComponent(sessionId)}¤cy=${currency}`;
|
|
76
|
+
const load = () => {
|
|
77
|
+
void fetch(endpoint, { cache: 'no-store' })
|
|
78
|
+
.then((res) => {
|
|
79
|
+
if (!res.ok)
|
|
80
|
+
throw new Error(`HTTP ${res.status}`);
|
|
81
|
+
return res.json();
|
|
82
|
+
})
|
|
83
|
+
.then((data) => {
|
|
84
|
+
if (!alive)
|
|
85
|
+
return;
|
|
86
|
+
setResponse(data);
|
|
87
|
+
setFailed(false);
|
|
88
|
+
})
|
|
89
|
+
.catch(() => {
|
|
90
|
+
if (alive)
|
|
91
|
+
setFailed(true);
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
load();
|
|
95
|
+
timer = window.setInterval(load, REFRESH_MS);
|
|
96
|
+
return () => {
|
|
97
|
+
alive = false;
|
|
98
|
+
if (timer !== undefined)
|
|
99
|
+
window.clearInterval(timer);
|
|
100
|
+
};
|
|
101
|
+
}, [sessionId, currency]);
|
|
102
|
+
const balance = response?.balance;
|
|
103
|
+
const balanceEnabled = response?.pricebook?.balanceEnabled !== false;
|
|
104
|
+
const subagents = response?.subagents;
|
|
105
|
+
const combined = combineTotals(cost?.totals, subagents);
|
|
106
|
+
const subSpend = subagentSpend(subagents);
|
|
107
|
+
// The dock readout must never vanish on a session switch: switching
|
|
108
|
+
// sessions re-binds the projection hook, and until the new session's
|
|
109
|
+
// baseline has carried the sessionCost key useProjection returns
|
|
110
|
+
// undefined (capability absent - never a usable signal). Treat a
|
|
111
|
+
// missing ledger as a zero ledger so the row renders CNY 0.00
|
|
112
|
+
// instead of disappearing while the baseline is in flight.
|
|
113
|
+
const mainSpend = cost?.totals?.cost ?? 0;
|
|
114
|
+
const parts = [];
|
|
115
|
+
try {
|
|
116
|
+
const symbol = currencySymbol(currency);
|
|
117
|
+
// Breakdown first (cached input, uncached input, output — combined with
|
|
118
|
+
// subagents), then the main session total, then the subagent total, then
|
|
119
|
+
// the balance.
|
|
120
|
+
if (combined !== undefined && combined.pricedSteps > 0) {
|
|
121
|
+
parts.push(t('line.cacheRead', { amount: `${symbol}${formatMoney(combined.cacheReadCost)}` }));
|
|
122
|
+
parts.push(t('line.uncached', { amount: `${symbol}${formatMoney(combined.uncachedCost)}` }));
|
|
123
|
+
parts.push(t('line.output', { amount: `${symbol}${formatMoney(combined.outputCost)}` }));
|
|
124
|
+
}
|
|
125
|
+
parts.push(t('cost.label', { amount: `${symbol}${formatMoney(mainSpend)}` }));
|
|
126
|
+
if (subSpend > 0) {
|
|
127
|
+
parts.push(t('line.subagent', { amount: `${symbol}${formatMoney(subSpend)}` }));
|
|
128
|
+
}
|
|
129
|
+
if (balanceEnabled && balance?.ok === true && balance.balance !== null) {
|
|
130
|
+
parts.push(t('balance.label', { amount: `${currencySymbol(balance.balance.currency)}${formatMoney(balance.balance.total)}` }));
|
|
131
|
+
}
|
|
132
|
+
else if (balanceEnabled && failed) {
|
|
133
|
+
parts.push(t('balance.failed'));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* keep the row alive */
|
|
138
|
+
}
|
|
139
|
+
if (parts.length === 0)
|
|
140
|
+
return null;
|
|
141
|
+
const model = cost?.model?.model;
|
|
142
|
+
const tooltipParts = useMemo(() => {
|
|
143
|
+
const rows = [];
|
|
144
|
+
try {
|
|
145
|
+
rows.push(...costDetail(cost, model, t, currency));
|
|
146
|
+
if (subSpend > 0) {
|
|
147
|
+
rows.push(t('line.subagent', { amount: `${currencySymbol(currency)}${formatMoney(subSpend)}` }));
|
|
148
|
+
}
|
|
149
|
+
const pricebook = response?.pricebook;
|
|
150
|
+
if (pricebook !== undefined && pricebook.current !== null) {
|
|
151
|
+
const source = pricebook.current.source;
|
|
152
|
+
if (source === 'official' || source === 'fallback' || source === 'manual' || source === 'openrouter') {
|
|
153
|
+
rows.push(t('pill.detail.source', { source: t(`source.${source}`) }));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (balanceEnabled && balance?.ok === true)
|
|
157
|
+
rows.push(balanceTooltip(balance, t));
|
|
158
|
+
if (response !== null) {
|
|
159
|
+
rows.push(t('refreshedAt', { time: formatTime(balance?.ok === true ? balance.fetchedAt : Date.now()) }));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
/* keep the row alive */
|
|
164
|
+
}
|
|
165
|
+
return rows;
|
|
166
|
+
}, [cost, model, t, balance, balanceEnabled, response, subSpend, currency]);
|
|
167
|
+
const line = parts.join(' · ');
|
|
168
|
+
const tooltip = tooltipParts.filter(Boolean).join(' · ');
|
|
169
|
+
if (tooltip.length === 0) {
|
|
170
|
+
return _jsx("div", { className: css.root, "data-testid": "session-cost-line", children: line });
|
|
171
|
+
}
|
|
172
|
+
return (_jsx(Tooltip, { label: tooltip, side: "top", delayMs: 500, children: _jsx("div", { className: css.root, "data-testid": "session-cost-line", children: line }) }));
|
|
173
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { UseProjection } from '@deepseek-ai/dsh-client-runtime/client';
|
|
2
|
+
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
+
import type { ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client';
|
|
4
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
|
+
export type PillLocale = PropsLocale<'cost-meter'>['t'];
|
|
6
|
+
export interface SessionCostPillProps {
|
|
7
|
+
useSession: SnapshotSelectorHook<ConversationSnapshot>;
|
|
8
|
+
useProjection: UseProjection;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
t: PillLocale;
|
|
11
|
+
}
|
|
12
|
+
export declare const SessionCostPill: import("react").MemoExoticComponent<({ useSession, useProjection, sessionId, t }: SessionCostPillProps) => import("react").JSX.Element | null>;
|
|
13
|
+
//# sourceMappingURL=SessionCostPill.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionCostPill.d.ts","sourceRoot":"","sources":["../../../src/client/SessionCostPill.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAC3E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAA;AAC5E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAA;AAClF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AASnE,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,CAAA;AAEvD,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,oBAAoB,CAAC,oBAAoB,CAAC,CAAA;IACtD,aAAa,EAAE,aAAa,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,CAAC,EAAE,UAAU,CAAA;CACd;AA6BD,eAAO,MAAM,eAAe,oFAA8E,oBAAoB,wCAgI5H,CAAA"}
|