From 229f7b15ac980a94259a5b3ac5c544322daf2b0a Mon Sep 17 00:00:00 2001 From: bowenliang123 Date: Wed, 21 Oct 2026 10:42:31 +0800 Subject: [PATCH] fix(cost): make DeepSeek peak pricing explicit and weekday-based The tooltip and shared type still described the pre-#78 scheme (peak at list price, off at half), while models.dev lists DeepSeek's off-peak rates and the estimator doubles the peak bucket. Correct the wording in both locales, the cost type, and the READMEs; only DeepSeek's peak buckets double, every other provider prices at list. Express the peak clock in Beijing Time directly (Monday-Friday 09:00-12:00 and 14:00-18:00, weekends off-peak) and rename isPeakUtc to isBeijingPeak. The UTC-to-Beijing refactor is behavior-preserving, so no stateVersion bump is needed. Statutory holidays are deliberately not modeled; record that limitation in docs/compatibility.md. Refs #178 --- README.md | 2 +- README_zh.md | 2 +- docs/compatibility.md | 1 + src/client/i18nEn.ts | 2 +- src/client/i18nZh.ts | 2 +- src/host/fold.ts | 2 +- src/host/foldMetering.ts | 12 +++++++----- src/shared/types.ts | 3 ++- tests/client/components/statsContext.spec.ts | 6 +++++- tests/host/activity.spec.ts | 10 +++++----- tests/host/fold-events.spec.ts | 4 ++-- 11 files changed, 27 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index be077be..ae22347 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Open any session and click the **Context / 上下文** tab: | Card | The question it answers | | --- | --- | -| **Context Stats** | Turns, steps, human inputs, live tool calls — plus a cost estimated from the models.dev list prices (hover the `?` for per-1M rates; DeepSeek peak/off-peak aware). | +| **Context Stats** | Turns, steps, human inputs, live tool calls — plus a cost estimated from the models.dev list prices (hover the `?` for per-1M rates; only DeepSeek splits peak/off-peak: its listed rates are the off-peak tier, and peak windows bill at 2×). | | **Token Stats** | Where the billed tokens went — the same total as the chat stats line under the composer, split by composition (system, tools, messages…) with the provider-exact output closing the ring, and the session's cache-hit rate in the corner. | | **Timing Stats** | How active time split across model calls, tool runs, and overhead. | | **Current Context** | What's in the window *right now*. | diff --git a/README_zh.md b/README_zh.md index c8b35ba..4d07a37 100644 --- a/README_zh.md +++ b/README_zh.md @@ -78,7 +78,7 @@ dsh plugin --profile web update dsh-context@latest | 卡片 | 解答的问题 | | --- | --- | -| **Context Stats** | 轮次、步骤、用户输入、实时工具调用——以及根据 models.dev 价格列表估算的成本(将鼠标悬停在 `?` 上可查看每百万 Token 单价;支持 DeepSeek 高峰 / 非高峰价格)。 | +| **Context Stats** | 轮次、步骤、用户输入、实时工具调用——以及根据 models.dev 价格列表估算的成本(将鼠标悬停在 `?` 上可查看每百万 Token 单价;仅 DeepSeek 区分高峰 / 非高峰:其挂牌价为谷时价,高峰时段按 2 倍计费)。 | | **Token Stats** | 计费 Token 用在了哪里——总量与输入框下方聊天统计行相同,按组成(system、tools、messages……)拆分,并以服务商精确报告的输出 Token 补足环形图,角落显示会话缓存命中率。 | | **Timing Stats** | 活跃时间如何分布在模型调用、工具运行和开销之间。 | | **Current Context** | 此刻窗口中的内容。 | diff --git a/docs/compatibility.md b/docs/compatibility.md index 24cfb23..fc867f1 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -60,6 +60,7 @@ Last verified: **2026-10-20** (dsh-context `0.73.3`, dsh `0.1.7-rc.2` / `0.2.0-r - **Limitation — declared divergence:** the timing card's wait/generation boundary opens at the first OBSERVABLE instant (the first token, or an earlier `block-start` marker the token packing could not stamp), not at the meter's first-token anchor — so marker-tiled decode buckets and the split never double-count one window. - **Limitation — daily-ledger attribution:** `contextTimeline.cost` prices at the settlement instant while the daily ledger prices at the step-initiation instant, so a tariff-boundary step can split its peak/off buckets differently between the lifetime total and the KPI band; the ledger also retains 400 days (`MAX_KEPT_DAYS` in `src/host/activity.ts`). +- **Limitation — statutory holidays:** no holiday calendar is maintained (by decision), so a holiday-weekday request inside the peak windows prices at peak where the platform bills off-peak; the tooltip states the weekday/weekend rule. (`src/host/foldMetering.ts` `isBeijingPeak`) ## 5 · Client surface diff --git a/src/client/i18nEn.ts b/src/client/i18nEn.ts index 32fc8da..0bc4b2a 100644 --- a/src/client/i18nEn.ts +++ b/src/client/i18nEn.ts @@ -46,7 +46,7 @@ export const DICT_EN = { 'stats.cacheHitTip': 'Cumulative cache-read share of billed input across the session.', 'stats.cost': 'Cost', 'stats.costTip': 'Estimated cost for this agent and every descendant subagent session, for reference only.', - 'stats.costTipDeepseek': 'DeepSeek bills peak windows (Beijing Time weekdays 09:00–12:00, 14:00–18:00) at list price; all other hours are half price.', + 'stats.costTipDeepseek': 'DeepSeek’s models.dev rates are its off-peak rates; peak windows (Beijing Time Monday–Friday 09:00–12:00 and 14:00–18:00) bill at twice the listed rate, while Saturday and Sunday are off-peak.', 'stats.costTipCny': 'The CNY display converts at 1 CNY = 0.15 USD.', 'stats.currentAgent': 'Current Agent', 'stats.teamSubs': 'Subagents × {n}', diff --git a/src/client/i18nZh.ts b/src/client/i18nZh.ts index 96c39c9..2ec1d97 100644 --- a/src/client/i18nZh.ts +++ b/src/client/i18nZh.ts @@ -43,7 +43,7 @@ export const DICT_ZH = { 'stats.cacheHitTip': '整个会话累计的缓存读取占计费输入的比例。', 'stats.cost': '费用', 'stats.costTip': '估算的费用,含当前智能体及所有层级的子智能体,仅供参考。', - 'stats.costTipDeepseek': 'DeepSeek 高峰时段(北京时间工作日 9:00–12:00、14:00–18:00)按挂牌价计费,其余时段半价。', + 'stats.costTipDeepseek': 'DeepSeek 在 models.dev 的挂牌价为谷时价;高峰时段(北京时间周一至周五 9:00–12:00、14:00–18:00)按挂牌价的 2 倍计费,周六、周日全天为谷时。', 'stats.costTipCny': '人民币按 1 元 = 0.15 美元换算。', 'stats.currentAgent': '当前 Agent', 'stats.teamSubs': '子 Agent × {n}', diff --git a/src/host/fold.ts b/src/host/fold.ts index e2d6aa2..3ff2966 100644 --- a/src/host/fold.ts +++ b/src/host/fold.ts @@ -765,6 +765,6 @@ export function applyTimeline(state: TimelineState, event: TimelineEvent, bounds // The pre-split surface, kept for the specs; production callers import the owning sibling directly. export { createTimelineState, trimToLastTurns } from './foldState' export type { TimelineEvent, TimelineState } from './foldState' -export { addBilledUsage, isPeakUtc, tokenCountOf } from './foldMetering' +export { addBilledUsage, isBeijingPeak, tokenCountOf } from './foldMetering' export type { BilledUsage, UsageLike } from './foldMetering' export { buildTimelineDetail, buildTimelineHead, buildTimelineView } from './foldWire' diff --git a/src/host/foldMetering.ts b/src/host/foldMetering.ts index 1a392da..0f91e57 100644 --- a/src/host/foldMetering.ts +++ b/src/host/foldMetering.ts @@ -40,13 +40,15 @@ export function tokenCountOf(value: unknown): number | null { return null } -/** DeepSeek's official peak windows: UTC 01:00-04:00 and 06:00-10:00, Monday through Friday; all other hours bill at half price. */ -export function isPeakUtc(time: number): boolean { - const at = new Date(time) +const BEIJING_OFFSET_MS = 8 * 3_600_000 + +/** DeepSeek's peak clock; statutory holidays are deliberately not modeled. */ +export function isBeijingPeak(time: number): boolean { + const at = new Date(time + BEIJING_OFFSET_MS) const day = at.getUTCDay() if (day === 0 || day === 6) return false const h = at.getUTCHours() - return (h >= 1 && h < 4) || (h >= 6 && h < 10) + return (h >= 9 && h < 12) || (h >= 14 && h < 18) } /** Clones along the mutated path only — the untouched branch stays shared with the persisted previous state, which the @@ -59,7 +61,7 @@ export function addBilledUsage( time: number, usage: BilledUsage, ): SessionCostUsage { - const period = isDeepSeekProvider(provider) && !isPeakUtc(time) ? 'off' : 'peak' + const period = isDeepSeekProvider(provider) && !isBeijingPeak(time) ? 'off' : 'peak' const models = prev?.[provider] ?? {} const periods = models[model] ?? {} const b = periods[period] ?? { uncached: 0, cacheRead: 0, cacheWrite: 0, output: 0 } diff --git a/src/shared/types.ts b/src/shared/types.ts index d59877d..3c4bbff 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -404,7 +404,8 @@ export interface TimingTotals { } /** One billed model's totals split by pricing period: providers without period pricing book - * everything under `peak`; DeepSeek bills `peak` at list price and `off` at half. */ + * everything under `peak`; DeepSeek's `peak` buckets price at twice the list (off-peak) rate, + * `off` buckets at list. */ export interface CostModelUsage { peak?: CostBucketTotals off?: CostBucketTotals diff --git a/tests/client/components/statsContext.spec.ts b/tests/client/components/statsContext.spec.ts index 23c092e..a0bb58b 100644 --- a/tests/client/components/statsContext.spec.ts +++ b/tests/client/components/statsContext.spec.ts @@ -325,6 +325,8 @@ describe('StatsContext', () => { assert.ok(costTip.includes('Cache write $0.15')) assert.ok(costTip.includes('Output $0.60')) assert.ok(costTip.includes('peak windows'), 'a DeepSeek session explains the peak/off-peak scheme') + assert.ok(costTip.includes('off-peak rates'), 'the tip says the listed rates are the off-peak tier') + assert.ok(costTip.includes('twice the listed rate'), 'the tip says peak bills at twice the listed rate') assert.ok(!costTip.includes('|'), 'no peak|off pairs — the footnotes carry the scheme') await m.unmount() }) @@ -430,6 +432,8 @@ describe('StatsContext', () => { const costTip = text(queryAll(m.container, '.lc-stat-tip')[1]) assert.ok(costTip.includes('含当前智能体及所有层级的子智能体'), 'the cost tip names the family scope too — all levels') assert.ok(costTip.includes('每百万 Token 价格')) + assert.ok(costTip.includes('挂牌价为谷时价'), 'the zh tip says the listed rates are off-peak') + assert.ok(costTip.includes('2 倍计费'), 'the zh tip says peak bills at twice the listed rate') assert.ok(costTip.includes('按 deepseek · deepseek-v4-flash 在 models.dev 的刊登价格如上。')) assert.ok(costTip.includes('人民币按 1 元 = 0.15 美元换算。'), 'the CNY display carries the conversion footnote') assert.ok(costTip.includes('缓存输入 ¥0.02')) @@ -536,7 +540,7 @@ describe('StatsContext', () => { // No row may show the peak|off-peak pair — that is DeepSeek's alone. const rows = queryAll(m.container, '.lc-stat-tip-row').map(el => text(el)) assert.ok(rows.every(r => !r.includes('|'))) - // Both buckets bill at list price: 2M × $0.075 — no half-price off-peak. + // Both buckets bill at list price (only DeepSeek's peak doubles): 2M × $0.075. assert.ok(headsOf(m.container).totals[3] === '2.0M/$0.15') await m.unmount() }) diff --git a/tests/host/activity.spec.ts b/tests/host/activity.spec.ts index 223827a..2c1813e 100644 --- a/tests/host/activity.spec.ts +++ b/tests/host/activity.spec.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict' import { describe, test } from 'vitest' import type { SessionEvent } from '@deepseek-ai/dsh-session' import { applyActivity, contextActivitySchema, createContextActivityDefinition } from '../../src/host/activity' -import { isPeakUtc } from '../../src/host/fold' +import { isBeijingPeak } from '../../src/host/fold' import { dayKeyOf } from '../../src/shared/days' /** Local noon of a day offset from 2026-01-01 — deterministic in every timezone. */ @@ -437,7 +437,7 @@ describe('applyActivity: the route in force', () => { state = applyActivity(state, assistantMessage(OFF_UTC, { inputTokens: 10 })) const day = state.days[dayKeyOf(OFF_UTC) ?? ''] assert.deepEqual(day?.cost, { '': { 'deepseek-v4': { peak: { uncached: 10, cacheRead: 0, cacheWrite: 0, output: 0 } } } }, - 'an unattributed provider is not DeepSeek, so no off-peak halving') + 'an unattributed provider is not DeepSeek, so its usage books under peak and is never doubled') }) }) @@ -469,7 +469,7 @@ describe('applyActivity: the initiation stamp', () => { '2026-01-01': { tokens: 9, requests: 1, - cost: { 'deepseek-official': { 'deepseek-v4': { [isPeakUtc(at(0)) ? 'peak' : 'off']: { uncached: 9, cacheRead: 0, cacheWrite: 0, output: 0 } } } }, + cost: { 'deepseek-official': { 'deepseek-v4': { [isBeijingPeak(at(0)) ? 'peak' : 'off']: { uncached: 9, cacheRead: 0, cacheWrite: 0, output: 0 } } } }, }, }) }) @@ -583,7 +583,7 @@ describe('applyActivity: the per-day pricing record', () => { state = applyActivity(state, assistantMessage(at(0), { inputTokens: 1, outputTokens: 2 })) state = applyActivity(state, assistantMessage(at(0), { inputTokens: 3 })) assert.deepEqual(state.days['2026-01-01'].cost, { - 'deepseek-official': { 'deepseek-v4': { [isPeakUtc(at(0)) ? 'peak' : 'off']: { uncached: 4, cacheRead: 0, cacheWrite: 0, output: 2 } } }, + 'deepseek-official': { 'deepseek-v4': { [isBeijingPeak(at(0)) ? 'peak' : 'off']: { uncached: 4, cacheRead: 0, cacheWrite: 0, output: 2 } } }, }) }) @@ -594,7 +594,7 @@ describe('applyActivity: the per-day pricing record', () => { assert.deepEqual(state.days['2026-01-01'], { tokens: 1, requests: 2, - cost: { 'deepseek-official': { 'deepseek-v4': { [isPeakUtc(at(0)) ? 'peak' : 'off']: { uncached: 1, cacheRead: 0, cacheWrite: 0, output: 0 } } } }, + cost: { 'deepseek-official': { 'deepseek-v4': { [isBeijingPeak(at(0)) ? 'peak' : 'off']: { uncached: 1, cacheRead: 0, cacheWrite: 0, output: 0 } } } }, }) }) diff --git a/tests/host/fold-events.spec.ts b/tests/host/fold-events.spec.ts index 6feafb8..07dd4e2 100644 --- a/tests/host/fold-events.spec.ts +++ b/tests/host/fold-events.spec.ts @@ -632,8 +632,8 @@ describe('unknown event types', () => { describe('session-cost accumulation', () => { const usage = { inputTokens: 100 } - // DeepSeek peak windows: UTC weekdays 01:00–04:00 and 06:00–10:00. 2024-01-01 - // is a Monday; 2024-01-06/07 the weekend. 02:00 UTC Monday is peak; the + // DeepSeek peak windows: Beijing Time weekdays 09:00–12:00 and 14:00–18:00 (UTC 01:00–04:00 and + // 06:00–10:00). 2024-01-01 is a Monday; 2024-01-06/07 the weekend. 02:00 UTC Monday is peak; the // helpers' default clock (1970-01-01T00:00:01 UTC) is off-peak. const PEAK = Date.UTC(2024, 0, 1, 2, 0, 0) const at100 = (seq: number, time: number) => assistantMessage(seq, { usage, time })