dsh-simple-usage-info 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.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * dsh-simple-usage-info — peak / off-peak pricing schedule types.
3
+ * @module dsh-simple-usage-info/pricing
4
+ */
5
+
6
+ /** Peak-rate window name. */
7
+ export declare const PEAK = 'peak'
8
+ /** Off-peak (discounted) window name. */
9
+ export declare const OFF_PEAK = 'off-peak'
10
+
11
+ /** Which window applies. */
12
+ export type PricingWindow = typeof PEAK | typeof OFF_PEAK
13
+ /**
14
+ * Why a window applies: a Chinese public holiday, a non-peak weekday, the peak
15
+ * clock windows, or none of them.
16
+ */
17
+ export type PricingReason = 'holiday' | 'non-peak-day' | 'peak-hours' | 'outside-peak-hours'
18
+
19
+ /** Raw schedule configuration, as it arrives from the Cordis patch. */
20
+ export interface PricingScheduleInput {
21
+ readonly peakWindows?: readonly string[]
22
+ readonly peakDays?: readonly number[]
23
+ readonly holidays?: readonly string[]
24
+ readonly makeupWorkdays?: readonly string[]
25
+ readonly makeupWorkdaysArePeak?: boolean
26
+ readonly useBundledHolidays?: boolean
27
+ readonly offPeakDiscountPercent?: number
28
+ }
29
+ /** A validated schedule. */
30
+ export interface NormalizedSchedule {
31
+ readonly windows: readonly { readonly label: string; readonly start: number; readonly end: number }[]
32
+ readonly peakDays: ReadonlySet<number>
33
+ /** The bundled State Council table, before any fetched data replaces it. */
34
+ readonly bundledHolidays: readonly string[]
35
+ readonly bundledMakeupWorkdays: readonly string[]
36
+ /** Dates the operator added; always additive, even over fetched data. */
37
+ readonly configuredHolidays: readonly string[]
38
+ readonly configuredMakeupWorkdays: readonly string[]
39
+ /** The effective holiday set. */
40
+ readonly holidays: ReadonlySet<string>
41
+ readonly makeupWorkdays: ReadonlySet<string>
42
+ readonly makeupWorkdaysArePeak: boolean
43
+ readonly offPeakDiscountPercent: number
44
+ /** The years the effective holiday data covers. */
45
+ readonly coveredYears: readonly number[]
46
+ /** Where the holiday dates came from: fetched, bundled, or none. */
47
+ readonly holidayData: 'remote' | 'bundled' | 'none'
48
+ /** The State Council papers the data was scraped from, when fetched. */
49
+ readonly holidayPapers: readonly string[]
50
+ readonly label: string
51
+ }
52
+
53
+ /**
54
+ * Validate raw configuration, seeding the holiday sets from the bundled table.
55
+ * @throws TypeError when a window, day, holiday, or discount is malformed.
56
+ */
57
+ export declare function normalizeSchedule(raw: PricingScheduleInput): NormalizedSchedule
58
+
59
+ /**
60
+ * Swap the bundled holiday table for a fetched snapshot, keeping configured
61
+ * dates additive. Returns the input unchanged when `remote` is undefined.
62
+ */
63
+ export declare function withHolidayData(
64
+ schedule: NormalizedSchedule,
65
+ remote: import('./holiday-source.js').HolidaySnapshot | undefined
66
+ ): NormalizedSchedule
67
+
68
+ /** The plain-JSON schedule the browser half repeats the rule from. */
69
+ export interface SerializedSchedule {
70
+ readonly peakWindows: readonly { readonly start: number; readonly end: number; readonly label: string }[]
71
+ readonly peakDays: readonly number[]
72
+ readonly holidays: readonly string[]
73
+ readonly makeupWorkdays: readonly string[]
74
+ readonly makeupWorkdaysArePeak: boolean
75
+ readonly offPeakDiscountPercent: number
76
+ readonly coveredYears: readonly number[]
77
+ }
78
+
79
+ /** Project a schedule into plain JSON for the browser half. */
80
+ export declare function serializeSchedule(schedule: NormalizedSchedule): SerializedSchedule
81
+
82
+ /** The pricing block the route returns. */
83
+ export interface PricingReading {
84
+ readonly window: PricingWindow
85
+ readonly reason: PricingReason
86
+ readonly discountPercent: number
87
+ readonly nextChangeAt?: number
88
+ readonly nextWindow?: PricingWindow
89
+ /** Human-readable UTC summary, e.g. `01:00-04:00, 06:00-10:00 UTC, Mon/Tue/...`. */
90
+ readonly schedule: string
91
+ /** The UTC year the reading was evaluated in. */
92
+ readonly holidayYear: number
93
+ /** Whether the effective holiday data covers `holidayYear`. */
94
+ readonly holidayCovered: boolean
95
+ /** The State Council paper or bundled notice for `holidayYear`, when known. */
96
+ readonly holidayNotice?: string
97
+ /** Where the holiday dates came from: fetched, bundled, or none. */
98
+ readonly holidayData: 'remote' | 'bundled' | 'none'
99
+ readonly now: number
100
+ /** The serializable schedule the browser half evaluates from. */
101
+ readonly windows: SerializedSchedule
102
+ }
103
+
104
+ /** Describe the pricing window in force at one instant. */
105
+ export declare function readPricing(schedule: NormalizedSchedule, instant: number): PricingReading
package/lib/pricing.js ADDED
@@ -0,0 +1,225 @@
1
+ /**
2
+ * DeepSeek peak / off-peak pricing windows.
3
+ *
4
+ * Policy, per https://api-docs.deepseek.com/quick_start/pricing :
5
+ *
6
+ * "Off-peak rates are half of the peak rates. Peak hours are 01:00 - 04:00 and
7
+ * 06:00 - 10:00 UTC, Monday through Friday, excluding Chinese public holidays.
8
+ * All other hours are off-peak, including weekends and Chinese public holidays
9
+ * in full."
10
+ *
11
+ * The windows are defined in UTC, so the price of a given instant is the same
12
+ * everywhere; only the clock the user reads it on differs. Everything here is
13
+ * therefore absolute-instant arithmetic, and the browser half repeats the same
14
+ * small rule to stay live between polls and to render the windows in the user's
15
+ * own timezone.
16
+ *
17
+ * Chinese public holidays come from `lib/holidays.js`, merged with any dates the
18
+ * operator adds in config.
19
+ *
20
+ * @module dsh-simple-usage-info/pricing
21
+ */
22
+ import { COVERED_YEARS, bundledHolidays, bundledMakeupWorkdays, noticeFor } from './holidays.js'
23
+
24
+ /** Peak-rate window name. */
25
+ export const PEAK = 'peak'
26
+ /** Off-peak (discounted) window name. */
27
+ export const OFF_PEAK = 'off-peak'
28
+
29
+ /** `HH:MM` -> minutes since UTC midnight. */
30
+ const CLOCK = /^([01]\d|2[0-3]):([0-5]\d)$/
31
+ /** `HH:MM-HH:MM` -> one window. */
32
+ const WINDOW = /^([01]\d|2[0-3]):([0-5]\d)-([01]\d|2[0-3]):([0-5]\d)$/
33
+
34
+ /** Minutes since UTC midnight for one `HH:MM`. */
35
+ function clockMinutes(text) {
36
+ const match = CLOCK.exec(text)
37
+ if (match === null) throw new TypeError(`clock "${text}" must be HH:MM in 24-hour UTC`)
38
+ return Number(match[1]) * 60 + Number(match[2])
39
+ }
40
+
41
+ /** `YYYY-MM-DD` for one instant's UTC date. */
42
+ function utcDayKey(instant) {
43
+ const date = new Date(instant)
44
+ const pad = (value) => String(value).padStart(2, '0')
45
+ return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`
46
+ }
47
+
48
+ /** Weekday names, indexed by `Date#getUTCDay`. */
49
+ const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
50
+
51
+ /**
52
+ * Validate raw configuration into the shape the readers below consume.
53
+ * Bundled Chinese public holidays are merged in unless `useBundledHolidays` is
54
+ * false; `holidays` is always additive, so it covers years the table does not.
55
+ * @param raw - the plugin's pricing configuration.
56
+ * @returns a detached, validated schedule.
57
+ * @throws TypeError when a window, day, holiday, or discount is malformed.
58
+ */
59
+ export function normalizeSchedule(raw) {
60
+ const windows = (raw.peakWindows ?? []).map((text) => {
61
+ const match = WINDOW.exec(text)
62
+ if (match === null) throw new TypeError(`peak window "${text}" must be HH:MM-HH:MM in 24-hour UTC`)
63
+ const start = clockMinutes(`${match[1]}:${match[2]}`)
64
+ const end = clockMinutes(`${match[3]}:${match[4]}`)
65
+ if (end <= start) throw new TypeError(`peak window "${text}" must not wrap past midnight; write overnight coverage as two windows`)
66
+ return { label: text, start, end }
67
+ })
68
+ const days = (raw.peakDays ?? []).map((day) => {
69
+ if (!Number.isInteger(day) || day < 0 || day > 6) throw new TypeError(`peak day ${String(day)} must be 0 (Sunday) through 6 (Saturday)`)
70
+ return day
71
+ })
72
+ const asDates = (values, label) =>
73
+ (values ?? []).map((day) => {
74
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) throw new TypeError(`${label} "${day}" must be YYYY-MM-DD`)
75
+ return day
76
+ })
77
+ const discount = raw.offPeakDiscountPercent ?? 50
78
+ if (typeof discount !== 'number' || discount < 0 || discount > 100) throw new TypeError(`offPeakDiscountPercent ${String(discount)} must be between 0 and 100`)
79
+
80
+ const useBundled = raw.useBundledHolidays !== false
81
+ // The bundled and configured dates are kept apart from the effective sets so
82
+ // `withHolidayData` can swap the bundled half for fetched data while leaving
83
+ // the operator's own additions in place.
84
+ const bundled = useBundled ? bundledHolidays() : []
85
+ const bundledMakeup = useBundled ? bundledMakeupWorkdays() : []
86
+ const configured = asDates(raw.holidays, 'holiday')
87
+ const configuredMakeup = asDates(raw.makeupWorkdays, 'makeup workday')
88
+
89
+ return {
90
+ windows,
91
+ peakDays: new Set(days),
92
+ bundledHolidays: bundled,
93
+ bundledMakeupWorkdays: bundledMakeup,
94
+ configuredHolidays: configured,
95
+ configuredMakeupWorkdays: configuredMakeup,
96
+ holidays: new Set([...bundled, ...configured]),
97
+ makeupWorkdays: new Set([...bundledMakeup, ...configuredMakeup]),
98
+ makeupWorkdaysArePeak: raw.makeupWorkdaysArePeak === true,
99
+ offPeakDiscountPercent: discount,
100
+ coveredYears: useBundled ? COVERED_YEARS : [],
101
+ holidayData: useBundled ? 'bundled' : 'none',
102
+ holidayPapers: [],
103
+ label: `${windows.map((window) => window.label).join(', ')} UTC, ${days.length === 0 ? 'no' : days.map((day) => DAY_NAMES[day]).join('/')}`
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Swap the bundled holiday table for fetched data.
109
+ *
110
+ * The fetched snapshot (see `lib/holiday-source.js`) replaces the bundled half
111
+ * entirely; dates the operator configured are always additive on top, so a
112
+ * private calendar can extend the official one.
113
+ * @param schedule - a normalized schedule.
114
+ * @param remote - the source's snapshot, or undefined to keep the bundled table.
115
+ * @returns a derived schedule; the input is not mutated.
116
+ */
117
+ export function withHolidayData(schedule, remote) {
118
+ if (remote === undefined) return schedule
119
+ return {
120
+ ...schedule,
121
+ holidays: new Set([...remote.holidays, ...schedule.configuredHolidays]),
122
+ makeupWorkdays: new Set([...remote.makeupWorkdays, ...schedule.configuredMakeupWorkdays]),
123
+ coveredYears: remote.years,
124
+ holidayData: 'remote',
125
+ holidayPapers: remote.papers
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Project a schedule into plain JSON for the browser half, which repeats the
131
+ * rule locally so the badge can flip on time and render in the user's timezone.
132
+ * @param schedule - a normalized schedule.
133
+ * @returns a serializable copy.
134
+ */
135
+ export function serializeSchedule(schedule) {
136
+ return {
137
+ peakWindows: schedule.windows.map((window) => ({ start: window.start, end: window.end, label: window.label })),
138
+ peakDays: [...schedule.peakDays].sort((left, right) => left - right),
139
+ holidays: [...schedule.holidays].sort(),
140
+ makeupWorkdays: [...schedule.makeupWorkdays].sort(),
141
+ makeupWorkdaysArePeak: schedule.makeupWorkdaysArePeak,
142
+ offPeakDiscountPercent: schedule.offPeakDiscountPercent,
143
+ coveredYears: [...schedule.coveredYears]
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Which window one instant falls in.
149
+ * @param schedule - a normalized schedule.
150
+ * @param instant - epoch milliseconds.
151
+ * @returns the window and why it applies.
152
+ */
153
+ function stateAt(schedule, instant) {
154
+ const date = new Date(instant)
155
+ const key = utcDayKey(instant)
156
+ if (schedule.holidays.has(key)) return { window: OFF_PEAK, reason: 'holiday' }
157
+ // The published rule says "Monday through Friday", so a 调休 makeup workday is
158
+ // off-peak unless the operator opts into treating it as a working day.
159
+ const makesUp = schedule.makeupWorkdaysArePeak && schedule.makeupWorkdays.has(key)
160
+ if (!makesUp && !schedule.peakDays.has(date.getUTCDay())) return { window: OFF_PEAK, reason: 'non-peak-day' }
161
+ const minutes = date.getUTCHours() * 60 + date.getUTCMinutes()
162
+ for (const window of schedule.windows) {
163
+ if (minutes >= window.start && minutes < window.end) return { window: PEAK, reason: 'peak-hours' }
164
+ }
165
+ return { window: OFF_PEAK, reason: 'outside-peak-hours' }
166
+ }
167
+
168
+ /**
169
+ * The first instant after `instant` whose window differs from the current one.
170
+ * Walks the candidate boundaries (UTC midnight plus every window edge) over the
171
+ * next eight days, which is enough to cross any weekend or holiday run.
172
+ * @param schedule - a normalized schedule.
173
+ * @param instant - epoch milliseconds.
174
+ * @returns the next boundary, or `undefined` when none is found.
175
+ */
176
+ function nextChange(schedule, instant) {
177
+ const base = new Date(instant)
178
+ const year = base.getUTCFullYear()
179
+ const month = base.getUTCMonth()
180
+ const day = base.getUTCDate()
181
+ const current = stateAt(schedule, instant).window
182
+ const candidates = []
183
+ for (let offset = 0; offset <= 8; offset += 1) {
184
+ const midnight = Date.UTC(year, month, day + offset, 0, 0, 0, 0)
185
+ candidates.push(midnight)
186
+ for (const window of schedule.windows) {
187
+ candidates.push(midnight + window.start * 60000)
188
+ candidates.push(midnight + window.end * 60000)
189
+ }
190
+ }
191
+ for (const candidate of candidates.filter((value) => value > instant).sort((left, right) => left - right)) {
192
+ const state = stateAt(schedule, candidate)
193
+ if (state.window !== current) return { at: candidate, window: state.window }
194
+ }
195
+ return { at: undefined, window: undefined }
196
+ }
197
+
198
+ /**
199
+ * Describe the pricing window in force at one instant.
200
+ * @param schedule - a normalized schedule.
201
+ * @param instant - epoch milliseconds to evaluate.
202
+ * @returns the window, its reason, the discount, the next transition, and the
203
+ * serializable schedule the browser half repeats the rule from.
204
+ */
205
+ export function readPricing(schedule, instant) {
206
+ const state = stateAt(schedule, instant)
207
+ const next = nextChange(schedule, instant)
208
+ const year = new Date(instant).getUTCFullYear()
209
+ return {
210
+ window: state.window,
211
+ reason: state.reason,
212
+ discountPercent: state.window === OFF_PEAK ? schedule.offPeakDiscountPercent : 0,
213
+ nextChangeAt: next.at,
214
+ nextWindow: next.window,
215
+ schedule: schedule.label,
216
+ holidayYear: year,
217
+ holidayCovered: schedule.coveredYears.includes(year),
218
+ // A fetched year cites the State Council paper; the bundled table cites its
219
+ // own notice. Either way the operator can see where the dates came from.
220
+ holidayNotice: schedule.holidayPapers?.[0] ?? noticeFor(year),
221
+ holidayData: schedule.holidayData,
222
+ now: instant,
223
+ windows: serializeSchedule(schedule)
224
+ }
225
+ }
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "dsh-simple-usage-info",
3
+ "version": "0.1.0",
4
+ "description": "DeepSeek Harness plugin: your DeepSeek API balance and the current peak/off-peak billing window, below the message input.",
5
+ "keywords": [
6
+ "dsh",
7
+ "deepseek-harness",
8
+ "deepseek",
9
+ "plugin",
10
+ "cordis",
11
+ "balance",
12
+ "usage",
13
+ "pricing",
14
+ "peak",
15
+ "off-peak",
16
+ "dsh-plugin"
17
+ ],
18
+ "license": "MIT",
19
+ "author": "Marjose Darang",
20
+ "homepage": "https://github.com/MarJose123/dsh-simple-usage-info#readme",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/MarJose123/dsh-simple-usage-info.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/MarJose123/dsh-simple-usage-info/issues"
27
+ },
28
+ "type": "module",
29
+ "main": "lib/index.js",
30
+ "types": "lib/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./lib/index.d.ts",
34
+ "default": "./lib/index.js"
35
+ },
36
+ "./client": {
37
+ "types": "./lib/client.d.ts",
38
+ "default": "./lib/client.js"
39
+ },
40
+ "./cordis.patch.yml": "./cordis.patch.yml",
41
+ "./package.json": "./package.json"
42
+ },
43
+ "files": [
44
+ "lib",
45
+ "cordis.patch.yml"
46
+ ],
47
+ "engines": {
48
+ "node": ">=20"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "scripts": {
54
+ "preflight": "node test/preflight-host.mjs && node test/preflight-pricing.mjs && node test/preflight-holidays.mjs && node test/preflight-client.mjs",
55
+ "preflight:host": "node test/preflight-host.mjs",
56
+ "preflight:pricing": "node test/preflight-pricing.mjs",
57
+ "preflight:holidays": "node test/preflight-holidays.mjs",
58
+ "preflight:client": "node test/preflight-client.mjs",
59
+ "preflight:live": "node test/preflight-live.mjs",
60
+ "preview:panel": "node test/preview-panel.mjs",
61
+ "verify:compose": "sh test/make-probe-home.sh && DSH_HOME=$PWD/tmp/probe-home dsh --profile web --dump-config | grep -A4 usage-info"
62
+ },
63
+ "dsh": {
64
+ "bundle": {
65
+ "patch": "./cordis.patch.yml"
66
+ },
67
+ "client": {
68
+ "inject": [
69
+ "@deepseek-ai/dsh-client-ui-conversation"
70
+ ],
71
+ "platform": "web"
72
+ }
73
+ },
74
+ "peerDependencies": {
75
+ "@deepseek-ai/cordis": "~4.0.4",
76
+ "@deepseek-ai/dsh-credentials": "0.2.0-rc.2",
77
+ "@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2",
78
+ "@deepseek-ai/schemastery": "~3.18.4"
79
+ },
80
+ "devDependencies": {
81
+ "@deepseek-ai/cordis": "~4.0.4",
82
+ "@deepseek-ai/dsh-credentials": "0.2.0-rc.2",
83
+ "@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2",
84
+ "@deepseek-ai/schemastery": "~3.18.4",
85
+ "react": "^18.3.1",
86
+ "react-dom": "^18.3.1"
87
+ }
88
+ }