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
This commit is contained in:
bowenliang123 committed 2026-10-21 10:42:31 +08:00
1 parent 18075e6b66
commit 229f7b15ac
11 files changed
+27 -19

No files matched your search

+1 -1
View File
@@ -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*. |
+1 -1
View File
@@ -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** | 此刻窗口中的内容。 |
+1
View File
@@ -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
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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}',
+1 -1
View File
@@ -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'
+7 -5
View File
@@ -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 }
+2 -1
View File
@@ -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
+5 -1
View File
@@ -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()
})
+5 -5
View File
@@ -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 } } } },
})
})
+2 -2
View File
@@ -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 })