dsh-all-usage 1.1.14 → 1.1.15

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/lib/http.js CHANGED
@@ -138,6 +138,7 @@ export function registerRoutes(host) {
138
138
  updatePricingState,
139
139
  persistPricing,
140
140
  syncPricing,
141
+ loadHolidayCalendar,
141
142
  } = host.pricing
142
143
  const { runBaseline } = host.sessionSync
143
144
  const { fetchBalance } = host.balance
@@ -239,6 +240,23 @@ export function registerRoutes(host) {
239
240
  sendJson(res, result.ok ? 200 : 502, result)
240
241
  },
241
242
  }))
243
+ ctx.effect(() => webServer.register({
244
+ kind: 'exact',
245
+ path: '/api/all-usage/pricing/holidays',
246
+ handler: async (req, res) => {
247
+ if (req.method !== 'POST') { res.statusCode = 405; res.end(); return }
248
+ if (!isTrustedLocalApiRequest(req, true) || !hasWriteToken(req, state.requestToken)) { rejectRequest(res); return }
249
+ const body = await readBody(req, 8 * 1024)
250
+ if (body.tooLarge) { sendJson(res, 413, { ok: false, message: 'request-too-large' }); return }
251
+ let args = null
252
+ try { args = JSON.parse(body.text) } catch (err) { /* invalid json */ }
253
+ const year = args !== null && typeof args === 'object' && !Array.isArray(args) ? Number(args.year) : NaN
254
+ if (!Number.isSafeInteger(year) || year < 2000 || year > 2100) { sendJson(res, 400, { ok: false, message: 'bad-holiday-request' }); return }
255
+ // Fetch only: the client freezes the returned dates into a pricing policy.
256
+ const result = await loadHolidayCalendar(year)
257
+ sendJson(res, result.ok ? 200 : 502, result)
258
+ },
259
+ }))
242
260
  ctx.effect(() => webServer.register({
243
261
  kind: 'exact',
244
262
  path: '/api/all-usage/status',
package/lib/plugin.js CHANGED
@@ -100,6 +100,7 @@ function apply(ctx) {
100
100
  ledgerWriteFailedSessions: new Set(),
101
101
  pricingState: createEmptyPricingState(),
102
102
  pricingResolutionCache: new Map(),
103
+ holidayCache: new Map(),
103
104
  pricingUnit: null,
104
105
  pricingReady: Promise.resolve(),
105
106
  pricingWriteChain: Promise.resolve(),
@@ -1,7 +1,12 @@
1
- import { COST_SCHEMA_VERSION, calculateCost, fetchModelsDevCatalog, normalizeCostSnapshot, normalizePricingState, officialProviderIds, serializeCostAggregate, serializePricingState, temporalPlanFor } from './pricing.js'
1
+ import { COST_SCHEMA_VERSION, builtinTemporalProfileFor, calculateCost, fetchHolidayCalendar, fetchModelsDevCatalog, normalizeCostSnapshot, normalizeModelId, normalizePricingState, officialProviderIds, serializeCostAggregate, serializePricingState, temporalPlanFor } from './pricing.js'
2
2
  import { billingInstantOf } from './usage-core.js'
3
3
 
4
4
  const LEDGER_VERSION = 3
5
+ // One price table row per usage identity; the cap keeps the panel payload bounded.
6
+ const MAX_PRICING_ROWS = 500
7
+ // The holiday arrangement changes at most once a year (and only for the next
8
+ // one); a day-long cache keeps the panel from re-fetching on every open.
9
+ const HOLIDAY_CALENDAR_TTL_MS = 24 * 60 * 60 * 1000
5
10
 
6
11
  export function createPricingRuntime(host) {
7
12
  const { state, markStatsChanged } = host
@@ -44,7 +49,7 @@ export function createPricingRuntime(host) {
44
49
  }
45
50
  for (const target of targets) addCostAggregateDirection(target, cost, direction)
46
51
  }
47
- function usedPricingModels(tierSchedules = null) {
52
+ function usedPricingModels(tierSchedules = null, temporalSchedules = null, detailed = false) {
48
53
  const rows = []
49
54
  const seen = new Set()
50
55
  const scheduleIds = new Map()
@@ -58,19 +63,58 @@ export function createPricingRuntime(host) {
58
63
  tierSchedules.push({ id, tiers: tiers.map((tier) => ({ ...tier })) })
59
64
  return id
60
65
  }
61
- for (const item of state.usageByStep.values()) {
62
- const identity = coerceIdentity(item.identity || item.modelId)
63
- if (seen.has(identity.identityKey)) continue
64
- seen.add(identity.identityKey)
65
- const resolved = resolveCurrentPricing(identity)
66
+ // Peak/off-peak plans are shared by every route priced from the same entry,
67
+ // so they travel once per distinct plan exactly like tier schedules do.
68
+ const temporalIds = new Map()
69
+ const temporalScheduleIdFor = (profile) => {
70
+ if (!Array.isArray(temporalSchedules) || profile === null || typeof profile !== 'object') return null
71
+ const policies = Array.isArray(profile.policies) ? profile.policies : []
72
+ if (policies.length === 0) return null
73
+ // Every attached policy carries the hash of its own content, so the plan
74
+ // identity is a cheap join instead of a full serialization per row
75
+ // (falling back to JSON when a hash is missing).
76
+ const key = policies.every((policy) => typeof policy.policyHash === 'string' && policy.policyHash !== '')
77
+ ? policies.map((policy) => policy.policyId + ':' + policy.policyHash).join('|')
78
+ : JSON.stringify(policies)
79
+ const existing = temporalIds.get(key)
80
+ if (existing !== undefined) return existing
81
+ const id = 'temporal-' + temporalSchedules.length
82
+ temporalIds.set(key, id)
83
+ temporalSchedules.push({ id, policies: policies.map((policy) => ({ ...policy })) })
84
+ return id
85
+ }
86
+ const providerKey = (value) => (typeof value === 'string' ? value.trim().toLowerCase() : '')
87
+ // Route identity keys are JSON tuples; rebuilding the exact tuple keeps a
88
+ // synthetic row on the same resolution-cache entry a real usage row uses.
89
+ const parseIdentityKey = (identityKey) => {
90
+ if (typeof identityKey !== 'string' || identityKey === '') return null
91
+ let parsed = null
92
+ try { parsed = JSON.parse(identityKey) } catch (err) { return null }
93
+ if (!Array.isArray(parsed) || parsed.length !== 4) return null
94
+ const textOrNull = (value) => (typeof value === 'string' && value.trim() !== '' ? value.trim() : null)
95
+ return { provider: textOrNull(parsed[0]), requestedModel: textOrNull(parsed[1]), actualModel: textOrNull(parsed[2]), legacyLabel: textOrNull(parsed[3]) }
96
+ }
97
+ // The price edit target of a row: the entry that is actually in effect. A
98
+ // priced row edits its resolved entry, an unpriced row gets a new
99
+ // provider-agnostic manual entry keyed by its own model id.
100
+ const basisKeyFor = (identity, resolved) => {
101
+ const priced = resolved.status === 'priced' && typeof resolved.pricingModel === 'string' && resolved.pricingModel !== ''
102
+ const modelId = priced ? resolved.pricingModel : (identity.actualModel || identity.requestedModel || resolved.pricingModel || identity.label || '')
103
+ const provider = priced ? resolved.providerId || '' : ''
104
+ return providerKey(provider) + '\u0000' + normalizeModelId(modelId)
105
+ }
106
+ const coveredBases = new Set()
107
+ const rowOf = (identity, resolved, options = {}) => {
66
108
  const tiers = Array.isArray(resolved.tiers) ? resolved.tiers : []
67
109
  const tierScheduleId = resolved.tiered === true ? scheduleIdFor(tiers) : null
68
- rows.push({
69
- identityKey: identity.identityKey,
110
+ const temporalScheduleId = detailed ? temporalScheduleIdFor(resolved.temporalProfile) : null
111
+ coveredBases.add(basisKeyFor(identity, resolved))
112
+ return {
113
+ identityKey: typeof options.identityKey === 'string' ? options.identityKey : identity.identityKey,
70
114
  provider: identity.provider,
71
115
  requestedModel: identity.requestedModel,
72
116
  actualModel: identity.actualModel,
73
- model: identity.label,
117
+ model: typeof options.model === 'string' && options.model !== '' ? options.model : identity.label,
74
118
  status: resolved.status,
75
119
  reason: resolved.reason || '',
76
120
  pricingModel: resolved.pricingModel || null,
@@ -84,12 +128,67 @@ export function createPricingRuntime(host) {
84
128
  tieredInvalid: resolved.tieredInvalid === true,
85
129
  inputTokenSemantics: resolved.inputTokenSemantics || 'fresh',
86
130
  multiplier: resolved.multiplier || '1',
131
+ temporalExplicit: resolved.temporalExplicit === true,
132
+ temporalBuiltin: resolved.temporalBuiltin === true,
133
+ temporalConfigInvalid: resolved.temporalConfigInvalid === true,
134
+ ...(detailed ? { usageBacked: options.usageBacked === true } : {}),
135
+ ...(temporalScheduleId === null ? {} : { temporalScheduleId }),
87
136
  ...(resolved.temporalRoute !== undefined && resolved.temporalRoute !== null ? { temporalRoute: resolved.temporalRoute } : {}),
88
137
  ...(resolved.temporalProfile && Array.isArray(resolved.temporalProfile.policies) && resolved.temporalProfile.policies.length > 0 ? { temporalPolicyId: String(resolved.temporalProfile.policies[0].policyId || ''), temporalTimezone: resolved.temporalProfile.policies[0].timezone === 'UTC' ? 'UTC' : null } : {}),
89
- })
90
- if (rows.length >= 500) break
138
+ }
91
139
  }
92
- rows.sort((a, b) => String(a.model).localeCompare(String(b.model)))
140
+ for (const item of state.usageByStep.values()) {
141
+ const identity = coerceIdentity(item.identity || item.modelId)
142
+ if (seen.has(identity.identityKey)) continue
143
+ seen.add(identity.identityKey)
144
+ rows.push(rowOf(identity, resolveCurrentPricing(identity), { usageBacked: true }))
145
+ if (rows.length >= MAX_PRICING_ROWS) break
146
+ }
147
+ if (detailed) {
148
+ // Configured rows the ledger never saw: an override or an
149
+ // identity-scoped mapping would otherwise become invisible and
150
+ // un-editable in the merged price table.
151
+ for (const mapping of state.pricingState.mappings) {
152
+ if (rows.length >= MAX_PRICING_ROWS) break
153
+ const identityKey = typeof mapping.identityKey === 'string' ? mapping.identityKey.trim() : ''
154
+ if (identityKey === '' || seen.has(identityKey)) continue
155
+ seen.add(identityKey)
156
+ const parsed = parseIdentityKey(identityKey)
157
+ const provider = parsed !== null ? parsed.provider : (mapping.provider === '' ? null : mapping.provider)
158
+ const model = parsed !== null ? (parsed.actualModel || parsed.requestedModel || parsed.legacyLabel) : (mapping.model !== '' ? mapping.model : (mapping.catalogModelId === '' ? null : mapping.catalogModelId))
159
+ const identity = {
160
+ identityKey,
161
+ provider,
162
+ requestedModel: parsed !== null ? parsed.requestedModel : model,
163
+ actualModel: parsed !== null ? parsed.actualModel : null,
164
+ label: (provider === null ? '' : provider + ' / ') + (model === null || model === '' ? identityKey : model),
165
+ legacy: parsed !== null && parsed.legacyLabel !== null && parsed.provider === null && parsed.requestedModel === null && parsed.actualModel === null,
166
+ }
167
+ rows.push(rowOf(identity, resolveCurrentPricing(identity), { identityKey, model: identity.label, usageBacked: false }))
168
+ }
169
+ for (const entry of state.pricingState.overrides) {
170
+ if (rows.length >= MAX_PRICING_ROWS) break
171
+ const key = providerKey(entry.providerId) + '\u0000' + normalizeModelId(entry.modelId)
172
+ if (coveredBases.has(key)) continue
173
+ coveredBases.add(key)
174
+ // A non-JSON key: it can never collide with a real route key (those are
175
+ // always JSON tuples), so a configured row cannot poison the shared
176
+ // resolution cache the way a constant key would.
177
+ const identity = {
178
+ identityKey: 'override:' + key,
179
+ provider: entry.providerId === '' ? null : entry.providerId,
180
+ requestedModel: entry.modelId,
181
+ actualModel: null,
182
+ label: entry.displayName || entry.modelId,
183
+ legacy: false,
184
+ }
185
+ rows.push(rowOf(identity, resolveCurrentPricing(identity), { identityKey: '', model: entry.displayName || entry.modelId, usageBacked: false }))
186
+ }
187
+ }
188
+ rows.sort((left, right) => {
189
+ if (left.usageBacked !== right.usageBacked) return left.usageBacked === true ? -1 : 1
190
+ return String(left.model).localeCompare(String(right.model))
191
+ })
93
192
  return rows
94
193
  }
95
194
  function pricingModelSearch(query, limit = 20) {
@@ -105,7 +204,20 @@ export function createPricingRuntime(host) {
105
204
  if (!modelId.includes(normalized) && !displayName.includes(normalized)) continue
106
205
  const score = modelId === normalized ? 0 : modelId.startsWith(normalized) ? 1 : displayName.startsWith(normalized) ? 2 : 3
107
206
  const previous = selected.get(entry.modelId)
108
- if (previous === undefined || score < previous.score) selected.set(entry.modelId, { value: entry.modelId, label: entry.displayName, providerId: entry.providerId, tiered: entry.tiered === true, tierCount: Array.isArray(entry.tiers) ? entry.tiers.length : 0, score })
207
+ // The mapping column previews the target price before saving, so the
208
+ // search result must carry the rates and the built-in peak/off-peak
209
+ // capability (never the whole catalog entry).
210
+ if (previous === undefined || score < previous.score) selected.set(entry.modelId, {
211
+ value: entry.modelId,
212
+ label: entry.displayName,
213
+ providerId: entry.providerId,
214
+ providerName: entry.providerName || '',
215
+ tiered: entry.tiered === true,
216
+ tierCount: Array.isArray(entry.tiers) ? entry.tiers.length : 0,
217
+ rates: { input: entry.input, output: entry.output, cacheRead: entry.cacheRead, cacheWrite: entry.cacheWrite },
218
+ builtinTemporal: builtinTemporalProfileFor(entry.modelId) !== null,
219
+ score,
220
+ })
109
221
  }
110
222
  return Array.from(selected.values()).sort((a, b) => a.score - b.score || a.value.localeCompare(b.value)).slice(0, Math.max(1, Math.min(50, Number.isInteger(limit) ? limit : 20))).map(({ score, ...entry }) => entry)
111
223
  }
@@ -113,6 +225,7 @@ export function createPricingRuntime(host) {
113
225
  const pricingStateSnapshot = state.pricingState
114
226
  const detailed = options.detailed !== false
115
227
  const tierSchedules = detailed ? [] : null
228
+ const temporalSchedules = detailed ? [] : null
116
229
  const snapshot = {
117
230
  schemaVersion: pricingStateSnapshot.schemaVersion,
118
231
  source: { ...pricingStateSnapshot.source },
@@ -121,16 +234,25 @@ export function createPricingRuntime(host) {
121
234
  overrideCount: pricingStateSnapshot.overrides.length,
122
235
  mappingCount: pricingStateSnapshot.mappings.length,
123
236
  configured: pricingStateSnapshot.catalogEntries.length > 0 || pricingStateSnapshot.overrides.length > 0 || pricingStateSnapshot.mappings.length > 0,
124
- usedModels: usedPricingModels(tierSchedules),
237
+ usedModels: usedPricingModels(tierSchedules, temporalSchedules, detailed),
125
238
  cost: serializeCostAggregate(state.totals.cost),
126
239
  }
127
240
  if (detailed) {
128
241
  snapshot.config = {
129
242
  sync: { ...pricingStateSnapshot.sync },
130
243
  mappings: pricingStateSnapshot.mappings.map((mapping) => ({ ...mapping })),
131
- overrides: pricingStateSnapshot.overrides.map(({ providerId, ...entry }) => ({ ...entry, ...(Array.isArray(entry.tiers) ? { tiers: entry.tiers.map((tier) => ({ ...tier })) } : {}) })),
244
+ // providerId and the peak/off-peak plan must round-trip: dropping
245
+ // providerId would turn a provider-scoped manual price into a
246
+ // provider-agnostic one on the next save (and would stop it from
247
+ // being found as a mapping target).
248
+ overrides: pricingStateSnapshot.overrides.map((entry) => ({
249
+ ...entry,
250
+ ...(Array.isArray(entry.tiers) ? { tiers: entry.tiers.map((tier) => ({ ...tier })) } : {}),
251
+ ...(entry.temporalPricing === undefined ? {} : { temporalPricing: JSON.parse(JSON.stringify(entry.temporalPricing)) }),
252
+ })),
132
253
  }
133
254
  snapshot.tierSchedules = tierSchedules
255
+ snapshot.temporalSchedules = temporalSchedules
134
256
  }
135
257
  return snapshot
136
258
  }
@@ -264,7 +386,7 @@ export function createPricingRuntime(host) {
264
386
  }
265
387
  function costSnapshotEquivalent(left, right) {
266
388
  if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') return false
267
- if (left.status !== right.status || left.currency !== right.currency || left.pricingModel !== right.pricingModel || left.providerId !== right.providerId || left.inputTokenSemantics !== right.inputTokenSemantics || left.multiplier !== right.multiplier || left.billableInputTokens !== right.billableInputTokens || left.billableOutputTokens !== right.billableOutputTokens || left.baseTotal !== right.baseTotal || left.total !== right.total || left.reason !== right.reason || left.tiered !== right.tiered || left.contextTokens !== right.contextTokens || left.pricingAt !== right.pricingAt || left.pricingTimeSource !== right.pricingTimeSource || left.pricingBand !== right.pricingBand || left.pricingPolicyId !== right.pricingPolicyId || left.pricingPolicyHash !== right.pricingPolicyHash || left.pricingTimezone !== right.pricingTimezone || left.temporalApplicable !== right.temporalApplicable || left.temporalExemptReason !== right.temporalExemptReason) return false
389
+ if (left.status !== right.status || left.currency !== right.currency || left.pricingModel !== right.pricingModel || left.providerId !== right.providerId || left.inputTokenSemantics !== right.inputTokenSemantics || left.multiplier !== right.multiplier || left.billableInputTokens !== right.billableInputTokens || left.billableOutputTokens !== right.billableOutputTokens || left.baseTotal !== right.baseTotal || left.total !== right.total || left.reason !== right.reason || left.tiered !== right.tiered || left.contextTokens !== right.contextTokens || left.pricingAt !== right.pricingAt || left.pricingTimeSource !== right.pricingTimeSource || left.pricingBand !== right.pricingBand || left.pricingHoliday !== right.pricingHoliday || left.pricingPolicyId !== right.pricingPolicyId || left.pricingPolicyHash !== right.pricingPolicyHash || left.pricingTimezone !== right.pricingTimezone || left.temporalApplicable !== right.temporalApplicable || left.temporalExemptReason !== right.temporalExemptReason) return false
268
390
  for (const key of ['input', 'output', 'cacheRead', 'cacheWrite']) {
269
391
  if (left.breakdown !== null && left.breakdown !== undefined && right.breakdown !== null && right.breakdown !== undefined) {
270
392
  if (left.breakdown[key] !== right.breakdown[key]) return false
@@ -377,6 +499,28 @@ export function createPricingRuntime(host) {
377
499
  schedulePricingSync()
378
500
  return { ...result, temporalReconciled: temporal.reconciled }
379
501
  }
502
+ /**
503
+ * Fetch one year of the Chinese holiday arrangement for the editor. Nothing is
504
+ * persisted here: the panel freezes the returned dates into a pricing policy,
505
+ * which is what makes later reconciliation reproducible.
506
+ */
507
+ async function loadHolidayCalendar(year) {
508
+ const cache = state.holidayCache instanceof Map ? state.holidayCache : (state.holidayCache = new Map())
509
+ const key = String(Number(year))
510
+ const cached = cache.get(key)
511
+ if (cached !== undefined && Date.now() - cached.fetchedAt < HOLIDAY_CALENDAR_TTL_MS) return { ok: true, cached: true, ...cached }
512
+ const result = await fetchHolidayCalendar(year)
513
+ if (!result.ok) return result
514
+ const entry = {
515
+ year: result.calendar.year,
516
+ dates: result.calendar.dates,
517
+ paperUrl: result.calendar.paperUrl,
518
+ sourceUrl: result.calendar.sourceUrl,
519
+ fetchedAt: result.calendar.fetchedAt,
520
+ }
521
+ cache.set(key, entry)
522
+ return { ok: true, cached: false, ...entry }
523
+ }
380
524
  function schedulePricingSync() {
381
525
  if (state.pricingSyncTimer !== null) { clearTimeout(state.pricingSyncTimer); state.pricingSyncTimer = null }
382
526
  if (state.disposed || state.pricingState.sync.autoEnabled !== true) return
@@ -401,6 +545,7 @@ export function createPricingRuntime(host) {
401
545
  backfillUnpricedCosts,
402
546
  reconcileTemporalPricing,
403
547
  updatePricingState,
404
- schedulePricingSync
548
+ schedulePricingSync,
549
+ loadHolidayCalendar
405
550
  }
406
551
  }
package/lib/pricing.js CHANGED
@@ -17,7 +17,18 @@ const MAX_CONTEXT_TIERS = 32
17
17
  const MAX_TEMPORAL_RULES = 16
18
18
  const MAX_TEMPORAL_WINDOWS = 16
19
19
  const MAX_TEMPORAL_POLICIES = 8
20
+ const MAX_TEMPORAL_HOLIDAYS = 400
20
21
  const TEMPORAL_TIMEZONE = 'UTC'
22
+ // Statutory-holiday dates are China Standard Time calendar days; a UTC policy
23
+ // band therefore runs from 16:00Z the day before to 16:00Z on the day itself.
24
+ const TEMPORAL_HOLIDAY_OFFSET_MS = 8 * 60 * 60 * 1000
25
+ // Public, no-key datasets of the State Council holiday arrangement (the file per
26
+ // year carries `days[].isOffDay`, and the official gov.cn notice in `papers`).
27
+ const HOLIDAY_CALENDAR_BASES = [
28
+ 'https://cdn.jsdelivr.net/gh/NateScarlet/holiday-cn@master/',
29
+ 'https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/',
30
+ ]
31
+ const MAX_HOLIDAY_CALENDAR_BYTES = 512 * 1024
21
32
  const TEMPORAL_TIME_SOURCES = ['request-context', 'usage-event', 'legacy-unknown']
22
33
  const RATE_KEYS = ['input', 'output', 'cacheRead', 'cacheWrite']
23
34
  const INPUT_SEMANTICS = ['legacy', 'total', 'fresh']
@@ -362,6 +373,60 @@ function normalizeTemporalWindows(raw) {
362
373
  return windows
363
374
  }
364
375
 
376
+ function normalizeHolidayDate(value) {
377
+ if (typeof value !== 'string') return null
378
+ const text = value.trim()
379
+ const match = text.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/)
380
+ if (match === null) return null
381
+ const year = Number(match[1])
382
+ const month = Number(match[2])
383
+ const day = Number(match[3])
384
+ if (year < 2000 || year > 2100) return null
385
+ const date = new Date(Date.UTC(year, month - 1, day))
386
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) return null
387
+ return text
388
+ }
389
+
390
+ /** Explicit statutory-holiday dates: sorted, de-duplicated, strictly shaped. */
391
+ function normalizeTemporalHolidays(raw) {
392
+ if (raw === undefined || raw === null) return []
393
+ if (!Array.isArray(raw)) return null
394
+ const seen = new Set()
395
+ const days = []
396
+ for (const candidate of raw) {
397
+ const text = normalizeHolidayDate(candidate)
398
+ if (text === null) return null
399
+ if (seen.has(text)) continue
400
+ seen.add(text)
401
+ days.push(text)
402
+ if (days.length > MAX_TEMPORAL_HOLIDAYS) return null
403
+ }
404
+ days.sort()
405
+ return days
406
+ }
407
+
408
+ const TEMPORAL_HOLIDAY_SETS = new WeakMap()
409
+ /** Cached lookup set for one policy's holiday list. */
410
+ function holidaySetFor(policy) {
411
+ if (!isRecord(policy)) return null
412
+ const holidays = Array.isArray(policy.holidays) ? policy.holidays : null
413
+ if (holidays === null || holidays.length === 0) return null
414
+ let set = TEMPORAL_HOLIDAY_SETS.get(policy)
415
+ if (set === undefined) {
416
+ set = new Set(holidays)
417
+ TEMPORAL_HOLIDAY_SETS.set(policy, set)
418
+ }
419
+ return set
420
+ }
421
+ /** True when the instant falls on one of the policy's statutory holidays (CST day). */
422
+ function holidayCoversInstant(policy, atMs) {
423
+ const set = holidaySetFor(policy)
424
+ if (set === null || !Number.isFinite(atMs)) return false
425
+ const local = new Date(Math.trunc(atMs) + TEMPORAL_HOLIDAY_OFFSET_MS)
426
+ const text = String(local.getUTCFullYear()) + '-' + String(local.getUTCMonth() + 1).padStart(2, '0') + '-' + String(local.getUTCDate()).padStart(2, '0')
427
+ return set.has(text)
428
+ }
429
+
365
430
  /** One versioned temporal policy: effective-windowed, UTC-band rules. */
366
431
  function normalizeTemporalPolicy(raw, entryDefaultRates = undefined) {
367
432
  if (!isRecord(raw)) return null
@@ -417,7 +482,13 @@ function normalizeTemporalPolicy(raw, entryDefaultRates = undefined) {
417
482
  if (list[index].startMinute < list[index - 1].endMinute) return null
418
483
  }
419
484
  }
420
- return { policyId, sourceUrl, timezone, effectiveFrom, effectiveUntil, defaultPlan, rules }
485
+ const holidays = normalizeTemporalHolidays(raw.holidays)
486
+ if (holidays === null) return null
487
+ // Provenance of a fetched holiday list. Deliberately NOT part of the policy
488
+ // hash: where the dates came from does not change what they price, but it is
489
+ // kept next to them so a frozen list can be traced back.
490
+ const holidaysSource = typeof raw.holidaysSource === 'string' ? raw.holidaysSource.trim().slice(0, 512) : ''
491
+ return { policyId, sourceUrl, timezone, effectiveFrom, effectiveUntil, defaultPlan, holidays, holidaysSource, rules }
421
492
  }
422
493
 
423
494
  /** Temporal pricing plan: an ordered, non-overlapping archive of policies. */
@@ -463,7 +534,23 @@ const DEEPSEEK_TEMPORAL_MODELS = new Map([
463
534
  ['deepseek-v4-pro', { input: '1.32', output: '3.96', cacheRead: '0.044', cacheWrite: '0', effectiveFrom: DEEPSEEK_TEMPORAL_EFFECTIVE_FROM }],
464
535
  ])
465
536
 
466
- function builtinTemporalProfileFor(modelId) {
537
+ // DeepSeek's own peak table excludes Chinese public holidays (`周一至周五(不含中国
538
+ // 法定节假日)`), so the built-in plan has to carry the arrangement itself; otherwise
539
+ // holiday requests are charged at peak rates. The list below is the 2026 arrangement
540
+ // from the State Council notice. Later years ship with a plugin update, and any plan
541
+ // (a built-in copy or a custom one) can be extended by hand or fetched at runtime
542
+ // through POST /api/all-usage/pricing/holidays.
543
+ const CHINA_HOLIDAY_SOURCE_2026 = 'https://www.gov.cn/zhengce/zhengceku/202511/content_7047091.htm'
544
+ const CHINA_HOLIDAYS_2026 = [
545
+ '2026-01-01', '2026-01-02', '2026-01-03', '2026-02-15', '2026-02-16', '2026-02-17',
546
+ '2026-02-18', '2026-02-19', '2026-02-20', '2026-02-21', '2026-02-22', '2026-02-23',
547
+ '2026-04-04', '2026-04-05', '2026-04-06', '2026-05-01', '2026-05-02', '2026-05-03',
548
+ '2026-05-04', '2026-05-05', '2026-06-19', '2026-06-20', '2026-06-21', '2026-09-25',
549
+ '2026-09-26', '2026-09-27', '2026-10-01', '2026-10-02', '2026-10-03', '2026-10-04',
550
+ '2026-10-05', '2026-10-06', '2026-10-07',
551
+ ]
552
+
553
+ export function builtinTemporalProfileFor(modelId) {
467
554
  const spec = DEEPSEEK_TEMPORAL_MODELS.get(normalizeModelId(modelId))
468
555
  if (spec === undefined) return null
469
556
  return {
@@ -474,6 +561,8 @@ function builtinTemporalProfileFor(modelId) {
474
561
  effectiveFrom: spec.effectiveFrom,
475
562
  effectiveUntil: null,
476
563
  defaultPlan: null,
564
+ holidays: CHINA_HOLIDAYS_2026,
565
+ holidaysSource: CHINA_HOLIDAY_SOURCE_2026 + ' · 2026 中国法定节假日安排',
477
566
  rules: [{ id: 'peak', weekdays: [1, 2, 3, 4, 5], windows: [{ startMinute: 60, endMinute: 240 }, { startMinute: 360, endMinute: 600 }], rates: { input: spec.input, output: spec.output, cacheRead: spec.cacheRead, cacheWrite: spec.cacheWrite } }],
478
567
  }],
479
568
  }
@@ -497,6 +586,8 @@ function temporalBand(profile, atMs) {
497
586
  if (!Number.isFinite(atMs) || atMs < 0) return null
498
587
  const policy = temporalPolicyFor(profile, atMs)
499
588
  if (policy === null) return null
589
+ // A statutory holiday is a full off-peak day: no rule can claim its instants.
590
+ if (holidayCoversInstant(policy, atMs)) return 'off-peak'
500
591
  const matched = matchingTemporalRule(policy, atMs)
501
592
  return matched === null ? 'off-peak' : 'peak'
502
593
  }
@@ -525,6 +616,7 @@ function temporalPolicyHash(policy) {
525
616
  policy.effectiveFrom,
526
617
  policy.effectiveUntil,
527
618
  policy.defaultPlan,
619
+ policy.holidays,
528
620
  policy.rules,
529
621
  ])
530
622
  return createHash('sha256').update(stable).digest('hex')
@@ -551,9 +643,12 @@ function temporalPlanFor(resolved, pricingAtMs) {
551
643
  const policy = temporalPolicyFor(profile, at)
552
644
  if (policy === null) return { status: 'history-gap', band: null, ruleRates: null, policyId: null, policyHash: null, timezone: null, exemptReason: null }
553
645
  const common = { policyId: policy.policyId || null, policyHash: policy.policyHash || null, timezone: policy.timezone === TEMPORAL_TIMEZONE ? TEMPORAL_TIMEZONE : null }
646
+ // A statutory holiday is a full off-peak day; its audit flag is stored with
647
+ // the cost snapshot so a ledger row can explain why it was off-peak.
648
+ if (holidayCoversInstant(policy, at)) return { status: 'applied', band: 'off-peak', ruleRates: policy.defaultPlan ? policy.defaultPlan.rates : null, holiday: true, exemptReason: null, ...common }
554
649
  const rule = matchingTemporalRule(policy, at)
555
- if (rule !== null) return { status: 'applied', band: 'peak', ruleRates: rule.rates, exemptReason: null, ...common }
556
- return { status: 'applied', band: 'off-peak', ruleRates: policy.defaultPlan ? policy.defaultPlan.rates : null, exemptReason: null, ...common }
650
+ if (rule !== null) return { status: 'applied', band: 'peak', ruleRates: rule.rates, holiday: false, exemptReason: null, ...common }
651
+ return { status: 'applied', band: 'off-peak', ruleRates: policy.defaultPlan ? policy.defaultPlan.rates : null, holiday: false, exemptReason: null, ...common }
557
652
  }
558
653
 
559
654
  function normalizeContextTier(raw, fallbackRates, legacySize = null) {
@@ -841,7 +936,15 @@ function attachTemporalProfile(entry, mapped, identity) {
841
936
  // fails closed instead of pricing with a plan the user did not write.
842
937
  if (entry.temporalPricingInvalid === true) return { invalid: true }
843
938
  let profile = isRecord(entry.temporalPricing) ? entry.temporalPricing : null
844
- if (profile === null && DEEPSEEK_OFFICIAL_PROVIDERS.includes(normalizeProvider(entry.providerId))) profile = builtinTemporalProfileFor(entry.modelId)
939
+ // The editor needs to tell a user-authored plan from the built-in DeepSeek
940
+ // table: only the former can be reset, the latter is re-attached on every
941
+ // resolution and must never be silently frozen into an override.
942
+ const explicit = profile !== null
943
+ let builtin = false
944
+ if (profile === null && DEEPSEEK_OFFICIAL_PROVIDERS.includes(normalizeProvider(entry.providerId))) {
945
+ profile = builtinTemporalProfileFor(entry.modelId)
946
+ builtin = profile !== null
947
+ }
845
948
  if (profile === null) return null
846
949
  const providerId = normalizeProvider(entry.providerId)
847
950
  const identityProvider = normalizeProvider(identity && identity.provider)
@@ -852,6 +955,8 @@ function attachTemporalProfile(entry, mapped, identity) {
852
955
  const route = DEEPSEEK_OFFICIAL_PROVIDERS.includes(providerId) && DEEPSEEK_OFFICIAL_PROVIDERS.includes(identityProvider) ? 'official' : mapped !== undefined && mapped !== null ? 'mapped' : 'other'
853
956
  return {
854
957
  route,
958
+ explicit,
959
+ builtin,
855
960
  profile: {
856
961
  policies: profile.policies.map((policy) => ({ ...policy, policyHash: temporalPolicyHash(policy) })),
857
962
  },
@@ -917,7 +1022,7 @@ function resolvePricing(identity, rawState) {
917
1022
  reasoningRateAvailable: entry.reasoningRateAvailable,
918
1023
  inputTokenSemantics: mapped && INPUT_SEMANTICS.includes(mapped.inputTokenSemantics) ? mapped.inputTokenSemantics : 'fresh',
919
1024
  multiplier: mapped ? mapped.multiplier : '1',
920
- ...(() => { const temporal = attachTemporalProfile(entry, mapped, identity); return temporal === null ? {} : temporal.invalid === true ? { temporalConfigInvalid: true } : { temporalProfile: temporal.profile, temporalRoute: temporal.route } })(),
1025
+ ...(() => { const temporal = attachTemporalProfile(entry, mapped, identity); return temporal === null ? {} : temporal.invalid === true ? { temporalConfigInvalid: true } : { temporalProfile: temporal.profile, temporalRoute: temporal.route, temporalExplicit: temporal.explicit === true, temporalBuiltin: temporal.builtin === true } })(),
921
1026
  }
922
1027
  }
923
1028
  const model = models[0] || null
@@ -1027,6 +1132,7 @@ function calculateCost(values, resolved, pricingAtMs = null, pricingTimeSource =
1027
1132
  pricingAt,
1028
1133
  pricingTimeSource: timeSource,
1029
1134
  pricingBand: plan.band,
1135
+ pricingHoliday: plan.holiday === true,
1030
1136
  pricingTimezone: plan.timezone,
1031
1137
  pricingPolicyId: plan.policyId,
1032
1138
  pricingPolicyHash: plan.policyHash,
@@ -1162,6 +1268,7 @@ function normalizeCostSnapshot(raw) {
1162
1268
  pricingAt,
1163
1269
  pricingTimeSource,
1164
1270
  pricingBand,
1271
+ pricingHoliday: raw.pricingHoliday === true,
1165
1272
  pricingTimezone,
1166
1273
  pricingPolicyId,
1167
1274
  pricingPolicyHash,
@@ -1206,6 +1313,69 @@ async function fetchModelsDevCatalog(fetchImpl = globalThis.fetch, options = {})
1206
1313
  }
1207
1314
  }
1208
1315
 
1316
+ /** Turn one holiday-cn dataset into the explicit off-day list the policy stores. */
1317
+ export function parseHolidayCalendar(raw, year) {
1318
+ if (!isRecord(raw)) return { ok: false, error: 'holiday-calendar-not-object' }
1319
+ if (raw.year !== undefined && raw.year !== null && Number(raw.year) !== Number(year)) return { ok: false, error: 'holiday-calendar-year-mismatch' }
1320
+ if (!Array.isArray(raw.days)) return { ok: false, error: 'holiday-calendar-days-missing' }
1321
+ const seen = new Set()
1322
+ const dates = []
1323
+ for (const day of raw.days) {
1324
+ if (!isRecord(day) || day.isOffDay !== true) continue
1325
+ const text = normalizeHolidayDate(day.date)
1326
+ if (text === null) return { ok: false, error: 'holiday-calendar-date-invalid' }
1327
+ if (seen.has(text)) continue
1328
+ seen.add(text)
1329
+ dates.push(text)
1330
+ }
1331
+ if (dates.length === 0) return { ok: false, error: 'holiday-calendar-empty' }
1332
+ if (dates.length > MAX_TEMPORAL_HOLIDAYS) return { ok: false, error: 'holiday-calendar-too-large' }
1333
+ dates.sort()
1334
+ const papers = Array.isArray(raw.papers) ? raw.papers.filter((item) => typeof item === 'string' && item.trim() !== '').map((item) => item.trim().slice(0, 512)) : []
1335
+ return { ok: true, calendar: { year: Number(year), dates, paperUrl: papers.length > 0 ? papers[0] : '', fetchedAt: 0 } }
1336
+ }
1337
+
1338
+ /**
1339
+ * Fetch one year of the Chinese holiday arrangement. The result is never applied
1340
+ * automatically: the caller freezes the returned dates into a pricing policy, so
1341
+ * later ledger reconciliation stays reproducible even if the upstream changes.
1342
+ */
1343
+ export async function fetchHolidayCalendar(year, fetchImpl = globalThis.fetch, options = {}) {
1344
+ const value = Number(year)
1345
+ if (!Number.isSafeInteger(value) || value < 2000 || value > 2100) return { ok: false, error: 'holiday-year-invalid' }
1346
+ if (typeof fetchImpl !== 'function') return { ok: false, error: 'fetch-unavailable' }
1347
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 15000
1348
+ const bases = Array.isArray(options.bases) && options.bases.length > 0 ? options.bases : HOLIDAY_CALENDAR_BASES
1349
+ let lastError = 'holiday-calendar-unavailable'
1350
+ for (const base of bases) {
1351
+ const url = String(base) + value + '.json'
1352
+ let controller = null
1353
+ let timer = null
1354
+ try {
1355
+ if (typeof AbortController === 'function') {
1356
+ controller = new AbortController()
1357
+ timer = setTimeout(() => controller.abort(), timeoutMs)
1358
+ }
1359
+ const response = await fetchImpl(url, { headers: { accept: 'application/json' }, signal: controller ? controller.signal : undefined })
1360
+ if (!response || response.ok !== true) { lastError = 'http-' + String((response && response.status) || 0); continue }
1361
+ const length = response.headers && typeof response.headers.get === 'function' ? Number(response.headers.get('content-length')) : 0
1362
+ if (Number.isFinite(length) && length > MAX_HOLIDAY_CALENDAR_BYTES) { lastError = 'holiday-calendar-too-large'; continue }
1363
+ const text = await response.text()
1364
+ if (new TextEncoder().encode(text).length > MAX_HOLIDAY_CALENDAR_BYTES) { lastError = 'holiday-calendar-too-large'; continue }
1365
+ let parsed = null
1366
+ try { parsed = JSON.parse(text.replace(/^\uFEFF/, '')) } catch (err) { lastError = 'holiday-calendar-invalid-json'; continue }
1367
+ const result = parseHolidayCalendar(parsed, value)
1368
+ if (!result.ok) { lastError = result.error; continue }
1369
+ return { ok: true, calendar: { ...result.calendar, sourceUrl: url, fetchedAt: Date.now() } }
1370
+ } catch (err) {
1371
+ lastError = 'holiday-calendar-fetch-failed'
1372
+ } finally {
1373
+ if (timer !== null) clearTimeout(timer)
1374
+ }
1375
+ }
1376
+ return { ok: false, error: lastError }
1377
+ }
1378
+
1209
1379
  export {
1210
1380
  COST_SCHEMA_VERSION,
1211
1381
  COST_STATUSES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-all-usage",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
4
4
  "description": "DeepSeek Harness usage dashboard with model, provider, workspace, cache, balance, and CSV insights",
5
5
  "repository": {
6
6
  "type": "git",