@gamegeek-saikel/dsh-cost-meter 0.2.0 → 0.3.0-alpha.1
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/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/lib/client.js +46 -71
- package/lib/client.js.map +1 -1
- package/lib/index.js +43 -38
- package/lib/types/client/AssistantCostChip.d.ts +8 -19
- package/lib/types/client/AssistantCostChip.d.ts.map +1 -1
- package/lib/types/client/AssistantCostChip.js +32 -38
- package/lib/types/client/CostPluginCard.d.ts +1 -1
- package/lib/types/client/CostPluginCard.d.ts.map +1 -1
- package/lib/types/client/CostView.d.ts +1 -1
- package/lib/types/client/CostView.d.ts.map +1 -1
- package/lib/types/client/SessionCostLine.d.ts +1 -4
- package/lib/types/client/SessionCostLine.d.ts.map +1 -1
- package/lib/types/client/SessionCostPill.d.ts +3 -3
- package/lib/types/client/SessionCostPill.d.ts.map +1 -1
- package/lib/types/client/SessionCostPill.js +8 -4
- package/lib/types/client/index.d.ts +8 -7
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +8 -5
- package/lib/types/index.d.ts +2 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +14 -12
- package/lib/types/pricing.d.ts +12 -9
- package/lib/types/pricing.d.ts.map +1 -1
- package/lib/types/pricing.js +27 -14
- package/lib/types/session-cost-projection.d.ts +9 -8
- package/lib/types/session-cost-projection.d.ts.map +1 -1
- package/lib/types/session-cost-projection.js +11 -17
- package/lib/types/types.d.ts +6 -0
- package/lib/types/types.d.ts.map +1 -1
- package/package.json +33 -29
- package/src/client/AssistantCostChip.tsx +37 -48
- package/src/client/CostPluginCard.tsx +1 -1
- package/src/client/CostView.tsx +1 -1
- package/src/client/SessionCostLine.tsx +1 -3
- package/src/client/SessionCostPill.tsx +12 -7
- package/src/client/index.ts +23 -12
- package/src/index.ts +15 -12
- package/src/pricing.ts +26 -14
- package/src/session-cost-projection.ts +12 -17
- package/src/types.ts +6 -0
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ Chat costs in DeepSeek pricing change over time (list prices, USD→CNY exchange
|
|
|
44
44
|
| Cost anchoring | Append-only pricebook snapshots; step cost computed once at the event's own time |
|
|
45
45
|
| Price sources | Manual override > official pricing page > built-in fallback > OpenRouter (fallback only, USD→CNY) > none |
|
|
46
46
|
| Official page | Parses both the 2026-08-21 combined zh/en tables (including the `deepseek-v4-flash-vision-exp` column and the English UTC schedule) and the legacy split tables; the built-in historical list still anchors `single` when the page no longer carries one |
|
|
47
|
-
| Peak pricing | 2026-08-17 00:00 Beijing rollout; peak
|
|
47
|
+
| Peak pricing | 2026-08-17 00:00 Beijing rollout; peak windows apply Monday–Friday only (the zh and en pages may state different timezones; fallback 09:00–12:00 / 14:00–18:00 Beijing), weekends and all other hours off-peak at half price |
|
|
48
48
|
| Cost formula | Uncached input + cache reads (hit rate) + cache writes (billed at uncached input rate) + output, per 1M tokens, CNY |
|
|
49
49
|
| Account balance | Official `GET /user/balance`, cached 60 s, single in-flight request, trust-fenced route |
|
|
50
50
|
| Subagent support | BFS over the live agent tree; conversation totals = main session + descendants |
|
|
@@ -72,7 +72,7 @@ The pricebook (`src/pricebook.ts`) is the durable price source, persisted on the
|
|
|
72
72
|
|
|
73
73
|
- **Priority chain** — per canonical model key (`provider/model`, bare model, or the `flash`/`pro` pricing key for DeepSeek-family models): manual override > official page > built-in fallback > OpenRouter (fallback only, USD→CNY, cache reads at the configured discount) > none.
|
|
74
74
|
- **Snapshot selection** — `snapshotForTime` picks the newest snapshot with `effectiveAt <= event time` (pre-install sessions anchor to the first snapshot once).
|
|
75
|
-
- **Peak/off-peak** — before the 2026-08-17 rollout all steps price at the single list price; after it, the band is chosen by the EVENT's own time against the schedule of the pricebook's own page (the zh and en pages each parse their own peak windows — the redesigned English page states UTC — falling back to Beijing 09:00–12:00 / 14:00–18:00, everything else off-peak). When the combined page has no legacy single-price column, `single` keeps anchoring to the built-in historical list; a page that still carries a separate legacy table wins. Each step's band is anchored once at fold time, so the per-reply cards and chips always show the band that round was billed at — never the band of the moment you are looking.
|
|
75
|
+
- **Peak/off-peak** — before the 2026-08-17 rollout all steps price at the single list price; after it, the band is chosen by the EVENT's own time against the schedule of the pricebook's own page (the zh and en pages each parse their own peak windows — the redesigned English page states UTC — falling back to Beijing 09:00–12:00 / 14:00–18:00, everything else off-peak). The official pages restrict peak windows to Monday–Friday, so Saturdays and Sundays are off-peak all day in the schedule's own timezone. When the combined page has no legacy single-price column, `single` keeps anchoring to the built-in historical list; a page that still carries a separate legacy table wins. Each step's band is anchored once at fold time, so the per-reply cards and chips always show the band that round was billed at — never the band of the moment you are looking.
|
|
76
76
|
- **Immutable ledger** — the `sessionCost` projection (`src/session-cost-projection.ts`) folds `request/header` (model) and usage-carrying events into per-step rows; a second usage sample for the same (turn, step) replaces the first (same-step finalization, not a re-price), with O(1) incremental totals.
|
|
77
77
|
|
|
78
78
|
## Project Structure
|
package/README.zh-CN.md
CHANGED
|
@@ -44,7 +44,7 @@ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、以及
|
|
|
44
44
|
| 成本锚定 | 只追加价格簿快照;步成本在事件自身时刻只计算一次 |
|
|
45
45
|
| 价格来源 | 手动覆盖 > 官方价格页 > 内置回退 > OpenRouter(仅回退,USD→CNY)> 无 |
|
|
46
46
|
| 官方页适配 | 同时解析 2026-08-21 新版中英文页面(含 `deepseek-v4-flash-vision-exp` 第三列与英文 UTC 时段)和旧版分表;无旧单价的页面继续用内置历史单价锚定 |
|
|
47
|
-
| 峰值定价 | 2026-08-17 00:00
|
|
47
|
+
| 峰值定价 | 2026-08-17 00:00 北京生效;高峰窗口仅周一至周五(中英文页面时区可能不同,缺省 09:00–12:00 / 14:00–18:00 北京),周六、周日全天及其余时段为半价闲时 |
|
|
48
48
|
| 成本公式 | 未命中输入 + 缓存命中(命中价)+ 缓存写入(按未命中输入价计)+ 输出,每百万 tokens,CNY |
|
|
49
49
|
| 账户余额 | 官方 `GET /user/balance`,缓存 60 秒,单飞请求,路由带信任围栏 |
|
|
50
50
|
| 子代理支持 | 沿活跃代理树 BFS;对话总花费 = 主会话 + 全部后代 |
|
|
@@ -72,7 +72,7 @@ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、以及
|
|
|
72
72
|
|
|
73
73
|
- **优先级链**——按规范模型键(`provider/model`、裸模型名,或 DeepSeek 系模型的 `flash`/`pro` 定价键):手动覆盖 > 官方页面 > 内置回退 > OpenRouter(仅回退,USD→CNY,缓存读按配置折扣)> 无。
|
|
74
74
|
- **快照选取**——`snapshotForTime` 取 `effectiveAt <= 事件时间` 的最新快照(安装前的会话一次性锚到首个快照)。
|
|
75
|
-
- **峰值/闲时**——2026-08-17 上线前所有步按单一价目计费;上线后按事件自身时刻与该价格簿页面的时段表选档(中英文页面各自解析自己的高峰窗口,英文新版为 UTC,抓取失败时回退到北京 09:00–12:00 / 14:00–18:00
|
|
75
|
+
- **峰值/闲时**——2026-08-17 上线前所有步按单一价目计费;上线后按事件自身时刻与该价格簿页面的时段表选档(中英文页面各自解析自己的高峰窗口,英文新版为 UTC,抓取失败时回退到北京 09:00–12:00 / 14:00–18:00,其余闲时)。官方页面限定高峰仅周一至周五,因此北京时间周六、周日全天为闲时。新版合并表没有旧单价列时,`single` 继续锚定内置历史单价;页面若有独立旧表,则优先用它。每步的档位在折叠时锚定一次,逐回复卡片与回复小标签永远显示该轮对话当时被计价的档位,而不是查看时刻的档位。
|
|
76
76
|
- **不可变账本**——`sessionCost` 投影(`src/session-cost-projection.ts`)把 `request/header`(模型)与携带用量的事件折叠为逐步记录;同一 (turn, step) 的第二次用量样本**替换**第一条(同一步终结,而非重新计价),总计以 O(1) 增量维护。
|
|
77
77
|
|
|
78
78
|
## 项目结构
|
package/lib/client.js
CHANGED
|
@@ -157,11 +157,13 @@ window.__ModuleLoader__.load({
|
|
|
157
157
|
const PEAK_PRICING_START_MS = Date.UTC(2026, 7, 16, 16, 0, 0);
|
|
158
158
|
/**
|
|
159
159
|
* Built-in fallback zh peak schedule: the official announcement's windows in
|
|
160
|
-
* Beijing time (peak 09:00-12:00 / 14:00-18:00, everything
|
|
160
|
+
* Beijing time (peak 09:00-12:00 / 14:00-18:00, Monday–Friday; everything
|
|
161
|
+
* else, including all of Saturday and Sunday, off-peak).
|
|
161
162
|
*/
|
|
162
163
|
const PEAK_SCHEDULE_ZH = {
|
|
163
164
|
timezone: "Asia/Shanghai",
|
|
164
|
-
ranges: [[9, 12], [14, 18]]
|
|
165
|
+
ranges: [[9, 12], [14, 18]],
|
|
166
|
+
weekdaysOnly: true
|
|
165
167
|
};
|
|
166
168
|
/** Built-in fallback USD prices from the English pricing page. */
|
|
167
169
|
const FALLBACK_CURRENT_USD = {
|
|
@@ -180,7 +182,9 @@ window.__ModuleLoader__.load({
|
|
|
180
182
|
/**
|
|
181
183
|
* Whether the given instant is a peak-pricing hour under one schedule: the
|
|
182
184
|
* zh fallback is Beijing time 09:00-12:00 and 14:00-18:00; each pricebook
|
|
183
|
-
* passes the schedule parsed from its own (zh or en) pricing page.
|
|
185
|
+
* passes the schedule parsed from its own (zh or en) pricing page. When the
|
|
186
|
+
* schedule is Monday–Friday only, Saturdays and Sundays in the schedule's
|
|
187
|
+
* timezone are off-peak all day.
|
|
184
188
|
* @param now - the instant to classify.
|
|
185
189
|
* @param schedule - the peak-hour schedule to classify against.
|
|
186
190
|
* @returns true during peak hours.
|
|
@@ -189,8 +193,13 @@ window.__ModuleLoader__.load({
|
|
|
189
193
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
190
194
|
timeZone: schedule.timezone,
|
|
191
195
|
hour: "numeric",
|
|
192
|
-
hour12: false
|
|
196
|
+
hour12: false,
|
|
197
|
+
weekday: "short"
|
|
193
198
|
}).formatToParts(now);
|
|
199
|
+
if (schedule.weekdaysOnly === true) {
|
|
200
|
+
const weekday = parts.find((part) => part.type === "weekday")?.value;
|
|
201
|
+
if (weekday === "Sat" || weekday === "Sun") return false;
|
|
202
|
+
}
|
|
194
203
|
const hour = Number(parts.find((part) => part.type === "hour")?.value);
|
|
195
204
|
if (Number.isNaN(hour)) return false;
|
|
196
205
|
return schedule.ranges.some(([start, end]) => hour >= start && hour < end);
|
|
@@ -314,28 +323,6 @@ window.__ModuleLoader__.load({
|
|
|
314
323
|
if (offPeakTotal === 0) return null;
|
|
315
324
|
return (peak.cacheReadPerMillion + peak.inputPerMillion + peak.outputPerMillion) / offPeakTotal;
|
|
316
325
|
}
|
|
317
|
-
/** CJK characters (the /1.5 density class). */
|
|
318
|
-
const CJK_RE = /[\u3400-\u4dbf\u4e00-\u9fff]/g;
|
|
319
|
-
/**
|
|
320
|
-
* Heuristic character→token estimate: CJK ÷ 1.5, everything else ÷ 4
|
|
321
|
-
* (the streaming estimate's output side).
|
|
322
|
-
* @param text - the accumulated text.
|
|
323
|
-
* @returns the estimated token count.
|
|
324
|
-
*/
|
|
325
|
-
function estimateTokens(text) {
|
|
326
|
-
const cjk = (text.match(CJK_RE) ?? []).length;
|
|
327
|
-
return Math.ceil(cjk / 1.5 + (text.length - cjk) / 4);
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Tokens of the streamed text/reasoning blocks of a partial assistant.
|
|
331
|
-
* @param blocks - the partial's content blocks (text/reasoning carry `text`).
|
|
332
|
-
* @returns the estimated token count.
|
|
333
|
-
*/
|
|
334
|
-
function blocksOutputTokens(blocks) {
|
|
335
|
-
let tokens = 0;
|
|
336
|
-
for (const block of blocks ?? []) if ((block.kind === "text" || block.kind === "reasoning") && typeof block.text === "string" && block.text.length > 0) tokens += estimateTokens(block.text);
|
|
337
|
-
return tokens;
|
|
338
|
-
}
|
|
339
326
|
/**
|
|
340
327
|
* The cache-read ratio of the newest completed step (input side of the
|
|
341
328
|
* estimate). Absent history conservatively prices all input as uncached
|
|
@@ -810,44 +797,28 @@ window.__ModuleLoader__.load({
|
|
|
810
797
|
//#endregion
|
|
811
798
|
//#region src/client/AssistantCostChip.tsx
|
|
812
799
|
/**
|
|
813
|
-
* Per-reply cost
|
|
814
|
-
* through the
|
|
815
|
-
*
|
|
816
|
-
* after the
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
800
|
+
* Per-reply cost at the end of the completed Turn's timing row. Rendered
|
|
801
|
+
* through the `conversation.chat.turnTail` chain slot — the selector only
|
|
802
|
+
* accepts closed Turns, so the extension appears once the reply settled and
|
|
803
|
+
* sits directly after the timing facts (用时 · 首 token · tok/s), with a dot
|
|
804
|
+
* separator and the same label-tertiary type as the clock text.
|
|
805
|
+
*
|
|
806
|
+
* DSH 0.1.5 removed the per-message assistant-actions slot and replaced the
|
|
807
|
+
* legacy chat-node window with turn/step locations; the cost is therefore
|
|
808
|
+
* addressed per Turn: the anchored step ledger (`sessionCost` projection) is
|
|
809
|
+
* filtered to the Turn's own steps and summed. Every price is the anchored
|
|
810
|
+
* snapshot value, never a current-price recompute; an unpriced Turn renders
|
|
821
811
|
* `—` (the Cost tab explains why).
|
|
822
812
|
*/
|
|
823
|
-
/**
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
* @param snapshot - the conversation snapshot.
|
|
827
|
-
* @param messageId - the addressed message identity.
|
|
828
|
-
* @returns the step coordinates, or null.
|
|
829
|
-
*/
|
|
830
|
-
function stepOfMessage(snapshot, messageId) {
|
|
831
|
-
const nodes = snapshot.chat.legacy.nodes;
|
|
832
|
-
for (let i = nodes.length - 1; i >= 0; i -= 1) {
|
|
833
|
-
const node = nodes[i];
|
|
834
|
-
if (node?.kind === "assistant" && node.messageId === messageId && node.turn !== void 0) return {
|
|
835
|
-
turn: node.turn,
|
|
836
|
-
step: node.step
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
return null;
|
|
813
|
+
/** The Turn's own ledger steps, in fold order. */
|
|
814
|
+
function stepsOfTurn(steps, turn) {
|
|
815
|
+
return steps?.filter((entry) => entry.turn === turn) ?? [];
|
|
840
816
|
}
|
|
841
|
-
|
|
842
|
-
function stepOf(steps, turn, step) {
|
|
843
|
-
return steps?.find((entry) => entry.turn === turn && entry.step === step);
|
|
844
|
-
}
|
|
845
|
-
const AssistantCostChip = (0, react.memo)(function AssistantCostChip({ messageId, useSession, useProjection, t }) {
|
|
817
|
+
const AssistantCostChip = (0, react.memo)(function AssistantCostChip({ matched, useProjection, t }) {
|
|
846
818
|
const currency = displayCurrency(t);
|
|
847
|
-
const step = useSession((snapshot) => stepOfMessage(snapshot, messageId));
|
|
848
819
|
const costCny = useProjection("sessionCost");
|
|
849
820
|
const costUsd = useProjection("sessionCostUsd");
|
|
850
|
-
const
|
|
821
|
+
const cost = currency === "USD" ? costUsd : costCny;
|
|
851
822
|
const [response, setResponse] = (0, react.useState)(null);
|
|
852
823
|
(0, react.useEffect)(() => {
|
|
853
824
|
let alive = true;
|
|
@@ -859,9 +830,11 @@ window.__ModuleLoader__.load({
|
|
|
859
830
|
alive = false;
|
|
860
831
|
};
|
|
861
832
|
}, [currency]);
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
833
|
+
const steps = stepsOfTurn(cost?.steps, matched.turn);
|
|
834
|
+
const priced = steps.filter((entry) => entry.cost !== null);
|
|
835
|
+
if (steps.length === 0) return null;
|
|
836
|
+
if (priced.length === 0) {
|
|
837
|
+
const reason = steps[0]?.unpricedReason === "NO_MODEL" ? t("reason.NO_MODEL") : t("reason.NO_PRICE");
|
|
865
838
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
866
839
|
className: AssistantCostChip_module_css_default.unpriced,
|
|
867
840
|
"data-cost-chip": true,
|
|
@@ -870,9 +843,12 @@ window.__ModuleLoader__.load({
|
|
|
870
843
|
children: t("chip.unpriced")
|
|
871
844
|
});
|
|
872
845
|
}
|
|
873
|
-
const
|
|
874
|
-
const
|
|
875
|
-
const
|
|
846
|
+
const total = priced.reduce((sum, entry) => sum + (entry.cost ?? 0), 0);
|
|
847
|
+
const amount = `${currencySymbol(currency)}${formatMoney(total)}`;
|
|
848
|
+
const last = priced[priced.length - 1];
|
|
849
|
+
if (last === void 0) return null;
|
|
850
|
+
const band = last.band ?? bandForTime(last.time);
|
|
851
|
+
const ratio = peakOffPeakMultiplier(response?.pricebook?.current ?? null, last.provider, last.model);
|
|
876
852
|
const bandLabel = band === "peak" ? ratio === null ? t("band.peak") : t("price.peakRatio", { multiplier: formatMultiplier(ratio) }) : band === "offPeak" ? ratio === null ? t("band.offPeak") : t("price.offPeakRatio", { multiplier: formatMultiplier(1 / ratio) }) : null;
|
|
877
853
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
878
854
|
className: AssistantCostChip_module_css_default.root,
|
|
@@ -925,7 +901,7 @@ window.__ModuleLoader__.load({
|
|
|
925
901
|
* ratio (conservative all-uncached without history), output at the
|
|
926
902
|
* character heuristic — both at CURRENT snapshot prices.
|
|
927
903
|
*/
|
|
928
|
-
/** Read the
|
|
904
|
+
/** Read the session's running flag (the pill's estimate gate). */
|
|
929
905
|
function runningOf(snapshot) {
|
|
930
906
|
return snapshot.running;
|
|
931
907
|
}
|
|
@@ -961,7 +937,7 @@ window.__ModuleLoader__.load({
|
|
|
961
937
|
const cost = currency === "USD" ? costUsd : costCny;
|
|
962
938
|
const pressure = useProjection("contextPressure");
|
|
963
939
|
const running = useSession(runningOf);
|
|
964
|
-
const partial =
|
|
940
|
+
const partial = null;
|
|
965
941
|
const [open, setOpen] = (0, react.useState)(false);
|
|
966
942
|
const [response, setResponse] = (0, react.useState)(null);
|
|
967
943
|
const wrapRef = (0, react.useRef)(null);
|
|
@@ -995,7 +971,7 @@ window.__ModuleLoader__.load({
|
|
|
995
971
|
if (!running || projection === void 0) return null;
|
|
996
972
|
const bucket = bucketAt(snapshot ?? void 0, model?.provider, model?.model ?? "unknown", Date.now(), schedule);
|
|
997
973
|
if (bucket === void 0) return null;
|
|
998
|
-
const stepEstimate = estimateCost(bucket, pressure?.projectedTokens ?? pressure?.pressureTokens ?? 0, cacheReadRatioOf(projection.steps),
|
|
974
|
+
const stepEstimate = estimateCost(bucket, pressure?.projectedTokens ?? pressure?.pressureTokens ?? 0, cacheReadRatioOf(projection.steps), 0);
|
|
999
975
|
return (combined?.cost ?? 0) + stepEstimate;
|
|
1000
976
|
}, [
|
|
1001
977
|
running,
|
|
@@ -1476,10 +1452,9 @@ window.__ModuleLoader__.load({
|
|
|
1476
1452
|
locale: NS,
|
|
1477
1453
|
label: () => t("view.cost")
|
|
1478
1454
|
}, CostView));
|
|
1479
|
-
ctx.slots.inject("conversation.chat.
|
|
1480
|
-
name: "conversation.chat.
|
|
1481
|
-
|
|
1482
|
-
order: 20,
|
|
1455
|
+
ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
|
|
1456
|
+
name: "conversation.chat.turnTail",
|
|
1457
|
+
select: (owner) => owner.turn.status === "closed" ? { turn: owner.turn.turn } : null,
|
|
1483
1458
|
locale: NS
|
|
1484
1459
|
}, AssistantCostChip));
|
|
1485
1460
|
ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
|