oc-deepseek-pricing 0.0.0-stage → 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 scicco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,210 @@
1
- # Temporary Holding Version
1
+ # oc-deepseek-pricing
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An OpenCode v2 TUI plugin that displays the current DeepSeek API pricing window in the prompt footer and session sidebar.
4
+
5
+ The indicator is intended for the direct DeepSeek provider. DeepSeek off-peak rates are half of peak rates.
6
+
7
+ ## Features
8
+
9
+ - Displays the current pricing window in the prompt footer.
10
+ - Displays pricing details in the session sidebar.
11
+ - Distinguishes peak and off-peak periods.
12
+ - Shows the next pricing transition and a countdown.
13
+ - Treats weekends as fully off-peak.
14
+ - Supports Chinese public-holiday overrides.
15
+ - Warns when the holiday calendar does not cover the current Chinese calendar year.
16
+ - Uses local computation only; no network request is needed at runtime.
17
+
18
+ ## Pricing schedule
19
+
20
+ Peak windows are evaluated in UTC:
21
+
22
+ - `01:00-04:00 UTC`, Monday through Friday.
23
+ - `06:00-10:00 UTC`, Monday through Friday.
24
+
25
+ All other times are off-peak, including:
26
+
27
+ - `04:00-06:00 UTC` on weekdays;
28
+ - all hours after `10:00 UTC` and before `01:00 UTC`;
29
+ - Saturdays and Sundays in full;
30
+ - Chinese public holidays in full.
31
+
32
+ The off-peak price multiplier is `0.5`.
33
+
34
+ ## Requirements
35
+
36
+ - OpenCode v2 with TUI plugin support.
37
+ - Bun.
38
+ - A terminal supported by OpenCode.
39
+
40
+ Development dependencies include:
41
+
42
+ - `@opencode/plugin`;
43
+ - `@opentui/core`;
44
+ - `@opentui/solid`;
45
+ - `solid-js`;
46
+ - TypeScript.
47
+
48
+ ## Repository structure
49
+
50
+ ```text
51
+ oc-deepseek-pricing/
52
+ ├── CALENDAR_UPDATE.md
53
+ ├── README.md
54
+ ├── bun.lock
55
+ ├── package.json
56
+ ├── src/
57
+ │ ├── holidays.ts
58
+ │ ├── pricing.test.ts
59
+ │ ├── pricing.ts
60
+ │ └── tui.tsx
61
+ └── tsconfig.json
62
+ ```
63
+
64
+ ## Development setup
65
+
66
+ Install dependencies:
67
+
68
+ ```sh
69
+ bun install
70
+ ```
71
+
72
+ Run the TypeScript check:
73
+
74
+ ```sh
75
+ bun check
76
+ ```
77
+
78
+ Run the test suite:
79
+
80
+ ```sh
81
+ bun test
82
+ ```
83
+
84
+ A successful validation should complete without TypeScript errors and with all pricing tests passing.
85
+
86
+ ## OpenCode CLI configuration
87
+
88
+ Register the local TUI package in:
89
+
90
+ ```text
91
+ ~/.config/opencode/cli.json
92
+ ```
93
+
94
+ Keep existing plugins and add `./plugins/oc-deepseek-pricing` to the `plugins` array.
95
+
96
+ Example:
97
+
98
+ ```json
99
+ {
100
+ "diffs": {
101
+ "wrap": "word",
102
+ "fullscreen": true
103
+ },
104
+ "session": {
105
+ "sidebar": "auto",
106
+ "scrollbar": false,
107
+ "thinking": "hide"
108
+ },
109
+ "animations": true,
110
+ "plugins": ["./herdr-opencode", "./plugins/oc-deepseek-pricing"]
111
+ }
112
+ ```
113
+
114
+ Relative paths are resolved from the OpenCode configuration directory.
115
+
116
+ The package exposes its TUI entrypoint through `package.json`:
117
+
118
+ ```json
119
+ {
120
+ "exports": {
121
+ "./tui": "./src/tui.tsx"
122
+ }
123
+ }
124
+ ```
125
+
126
+ The legacy or integration-specific `tui.jsonc` file does not need to contain this plugin when the package is loaded through `cli.json`.
127
+
128
+ ## Restarting OpenCode
129
+
130
+ After changing the plugin source or configuration, close the OpenCode TUI completely and start it again.
131
+
132
+ Opening a new conversation is not necessarily sufficient because plugin entrypoints are loaded during TUI startup.
133
+
134
+ ## Expected interface
135
+
136
+ When loaded, the prompt footer displays a compact indicator similar to:
137
+
138
+ ```text
139
+ DeepSeek: OFF-PEAK 50% · PEAK in 11h 36m
140
+ ```
141
+
142
+ During a peak window it displays a value similar to:
143
+
144
+ ```text
145
+ DeepSeek: PEAK · OFF-PEAK 50% in 1h 15m
146
+ ```
147
+
148
+ The session sidebar displays details similar to:
149
+
150
+ ```text
151
+ DeepSeek pricing
152
+ Status OFF-PEAK 50%
153
+ Reason outside peak windows
154
+ Next PEAK in 11h 36m
155
+ At 10/28/2026, 2:00:00 AM
156
+ China 2026-10-27
157
+ ```
158
+
159
+ The exact local transition time depends on the operating-system timezone and daylight-saving rules. Pricing classification itself is based on UTC, while holiday lookup uses the `Asia/Shanghai` calendar date.
160
+
161
+ ## Chinese public holidays
162
+
163
+ The holiday data is stored in:
164
+
165
+ ```text
166
+ src/holidays.ts
167
+ ```
168
+
169
+ Only officially verified calendar years should be added to `COVERED_HOLIDAY_YEARS`.
170
+
171
+ For the full annual update procedure, source requirements, timezone rules, validation checklist, and required tests, see [CALENDAR_UPDATE.md](./CALENDAR_UPDATE.md).
172
+
173
+ If the current Chinese calendar year is not covered, the plugin continues to apply the normal UTC weekday and weekend schedule but displays a coverage warning. Do not suppress the warning by adding a year without adding and reviewing the complete official calendar.
174
+
175
+ ## Tests
176
+
177
+ The test suite covers:
178
+
179
+ - the first weekday peak window;
180
+ - the off-peak gap between the two peak windows;
181
+ - the second weekday peak window;
182
+ - exact peak-window boundaries;
183
+ - weekends;
184
+ - ordinary weekday off-peak periods;
185
+ - Chinese public-holiday overrides;
186
+ - calendar-year coverage;
187
+ - next-transition calculation;
188
+ - weekend skipping when finding the next peak period;
189
+ - duration formatting;
190
+ - invalid date handling.
191
+
192
+ Run:
193
+
194
+ ```sh
195
+ bun test
196
+ ```
197
+
198
+ ## Type checking
199
+
200
+ Run:
201
+
202
+ ```sh
203
+ bun check
204
+ ```
205
+
206
+ The repository uses `NodeNext` module resolution. Relative TypeScript imports must therefore use the emitted JavaScript extension, for example:
207
+
208
+ ```ts
209
+ import { getPricingSnapshot } from "./pricing.js"
210
+ ```
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "oc-deepseek-pricing",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "OpenCode TUI plugin showing DeepSeek peak and off-peak pricing windows",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "opencode",
9
+ "opencode-plugin",
10
+ "deepseek",
11
+ "pricing",
12
+ "tui"
13
+ ],
14
+ "files": [
15
+ "src",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "exports": {
20
+ "./tui": "./src/tui.tsx"
21
+ },
22
+ "opencode": {
23
+ "tui": "./src/tui.tsx"
24
+ },
25
+ "scripts": {
26
+ "test": "bun test",
27
+ "check": "bun run format:check && bun run lint && bun run typecheck && bun test",
28
+ "format": "biome format --write .",
29
+ "format:check": "biome format .",
30
+ "lint": "biome lint .",
31
+ "lint:fix": "biome lint --write .",
32
+ "biome:check": "biome check .",
33
+ "biome:fix": "biome check --write .",
34
+ "typecheck": "tsc --noEmit",
35
+ "pack:check": "npm pack --dry-run",
36
+ "prepublishOnly": "bun run check"
37
+ },
38
+ "peerDependencies": {
39
+ "@opencode/plugin": ">=2.2.0 <2.3.0",
40
+ "@opentui/core": "0.7.x",
41
+ "@opentui/solid": "0.7.x",
42
+ "solid-js": "1.9.17"
43
+ },
44
+ "devDependencies": {
45
+ "@biomejs/biome": "2.6.3",
46
+ "@opencode/plugin": "2.2.0",
47
+ "@opentui/core": "0.7.0",
48
+ "@opentui/solid": "0.7.0",
49
+ "@types/bun": "1.4.4",
50
+ "solid-js": "1.9.17",
51
+ "typescript": "5.9.3"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/scicco/oc-deepseek-pricing.git"
56
+ },
57
+ "bugs": {
58
+ "url": "https://github.com/scicco/oc-deepseek-pricing/issues"
59
+ },
60
+ "homepage": "https://github.com/scicco/oc-deepseek-pricing#readme"
61
+ }
@@ -0,0 +1,56 @@
1
+ // Official public-holiday periods published by the General Office of the
2
+ // State Council of the People's Republic of China.
3
+ //
4
+ // Dates use the Asia/Shanghai calendar and ISO YYYY-MM-DD format.
5
+ // Compensatory working weekends are intentionally not listed: DeepSeek's
6
+ // pricing rule explicitly treats all Saturdays and Sundays as off-peak.
7
+ export const CHINESE_PUBLIC_HOLIDAYS = new Set<string>([
8
+ // 2026 New Year's Day: January 1-3
9
+ "2026-01-01",
10
+ "2026-01-02",
11
+ "2026-01-03",
12
+
13
+ // 2026 Spring Festival: February 15-23
14
+ "2026-02-15",
15
+ "2026-02-16",
16
+ "2026-02-17",
17
+ "2026-02-18",
18
+ "2026-02-19",
19
+ "2026-02-20",
20
+ "2026-02-21",
21
+ "2026-02-22",
22
+ "2026-02-23",
23
+
24
+ // 2026 Qingming Festival: April 4-6
25
+ "2026-04-04",
26
+ "2026-04-05",
27
+ "2026-04-06",
28
+
29
+ // 2026 Labor Day: May 1-5
30
+ "2026-05-01",
31
+ "2026-05-02",
32
+ "2026-05-03",
33
+ "2026-05-04",
34
+ "2026-05-05",
35
+
36
+ // 2026 Dragon Boat Festival: June 19-21
37
+ "2026-06-19",
38
+ "2026-06-20",
39
+ "2026-06-21",
40
+
41
+ // 2026 Mid-Autumn Festival: September 25-27
42
+ "2026-09-25",
43
+ "2026-09-26",
44
+ "2026-09-27",
45
+
46
+ // 2026 National Day: October 1-7
47
+ "2026-10-01",
48
+ "2026-10-02",
49
+ "2026-10-03",
50
+ "2026-10-04",
51
+ "2026-10-05",
52
+ "2026-10-06",
53
+ "2026-10-07",
54
+ ])
55
+
56
+ export const COVERED_HOLIDAY_YEARS = new Set<number>([2026])
@@ -0,0 +1,121 @@
1
+ import { describe, expect, test } from "bun:test"
2
+
3
+ import { formatDuration, getPricingSnapshot } from "./pricing.js"
4
+
5
+ const COVERED_YEARS = new Set([2026])
6
+
7
+ function snapshotAt(iso: string, holidays: ReadonlySet<string> = new Set()) {
8
+ return getPricingSnapshot(new Date(iso), {
9
+ chinesePublicHolidays: holidays,
10
+ coveredHolidayYears: COVERED_YEARS,
11
+ })
12
+ }
13
+
14
+ function stateAt(iso: string) {
15
+ return snapshotAt(iso).state
16
+ }
17
+
18
+ describe("pricing windows", () => {
19
+ test("handles both weekday peak windows", () => {
20
+ expect(stateAt("2026-10-26T00:59:59Z")).toBe("off-peak")
21
+ expect(stateAt("2026-10-26T01:00:00Z")).toBe("peak")
22
+ expect(stateAt("2026-10-26T04:00:00Z")).toBe("off-peak")
23
+ expect(stateAt("2026-10-26T06:00:00Z")).toBe("peak")
24
+ expect(stateAt("2026-10-26T10:00:00Z")).toBe("off-peak")
25
+ })
26
+
27
+ test("keeps weekends off-peak", () => {
28
+ const saturday = snapshotAt("2026-10-24T02:00:00Z")
29
+ const sunday = snapshotAt("2026-10-25T07:00:00Z")
30
+
31
+ expect(saturday.state).toBe("off-peak")
32
+ expect(saturday.reason).toBe("weekend")
33
+
34
+ expect(sunday.state).toBe("off-peak")
35
+ expect(sunday.reason).toBe("weekend")
36
+ })
37
+
38
+ test("marks ordinary weekday hours outside the windows as off-peak", () => {
39
+ const snapshot = snapshotAt("2026-10-26T05:00:00Z")
40
+
41
+ expect(snapshot.state).toBe("off-peak")
42
+ expect(snapshot.reason).toBe("outside-peak-window")
43
+ expect(snapshot.rateMultiplier).toBe(0.5)
44
+ })
45
+
46
+ test("uses the full multiplier during peak hours", () => {
47
+ const snapshot = snapshotAt("2026-10-26T08:00:00Z")
48
+
49
+ expect(snapshot.state).toBe("peak")
50
+ expect(snapshot.reason).toBe("peak-window")
51
+ expect(snapshot.rateMultiplier).toBe(1)
52
+ })
53
+ })
54
+
55
+ describe("Chinese public holidays", () => {
56
+ test("overrides a weekday peak window", () => {
57
+ const snapshot = snapshotAt("2026-10-01T02:00:00Z", new Set(["2026-10-01"]))
58
+
59
+ expect(snapshot.state).toBe("off-peak")
60
+ expect(snapshot.reason).toBe("chinese-public-holiday")
61
+ expect(snapshot.rateMultiplier).toBe(0.5)
62
+ })
63
+
64
+ test("reports whether the Chinese calendar year is covered", () => {
65
+ expect(snapshotAt("2026-10-27T12:00:00Z").holidayCalendarCovered).toBe(true)
66
+
67
+ const uncovered = getPricingSnapshot(new Date("2027-01-05T12:00:00Z"), {
68
+ chinesePublicHolidays: new Set(),
69
+ coveredHolidayYears: COVERED_YEARS,
70
+ })
71
+
72
+ expect(uncovered.holidayCalendarCovered).toBe(false)
73
+ })
74
+ })
75
+
76
+ describe("next transition", () => {
77
+ test("finds the end of the first peak window", () => {
78
+ const snapshot = snapshotAt("2026-10-27T02:15:00Z")
79
+
80
+ expect(snapshot.nextState).toBe("off-peak")
81
+ expect(snapshot.nextTransition).toBe("2026-10-27T04:00:00.000Z")
82
+ expect(snapshot.secondsUntilTransition).toBe(6_300)
83
+ })
84
+
85
+ test("finds the beginning of the second peak window", () => {
86
+ const snapshot = snapshotAt("2026-10-27T05:30:00Z")
87
+
88
+ expect(snapshot.nextState).toBe("peak")
89
+ expect(snapshot.nextTransition).toBe("2026-10-27T06:00:00.000Z")
90
+ expect(snapshot.secondsUntilTransition).toBe(1_800)
91
+ })
92
+
93
+ test("skips the weekend when finding the next peak window", () => {
94
+ const snapshot = snapshotAt("2026-10-30T10:00:00Z")
95
+
96
+ expect(snapshot.state).toBe("off-peak")
97
+ expect(snapshot.nextState).toBe("peak")
98
+ expect(snapshot.nextTransition).toBe("2026-11-02T01:00:00.000Z")
99
+ })
100
+ })
101
+
102
+ describe("duration formatting", () => {
103
+ test("formats minutes and hours", () => {
104
+ expect(formatDuration(60)).toBe("1m")
105
+ expect(formatDuration(3_600)).toBe("1h")
106
+ expect(formatDuration(5_400)).toBe("1h 30m")
107
+ })
108
+
109
+ test("rounds partial minutes up", () => {
110
+ expect(formatDuration(1)).toBe("1m")
111
+ expect(formatDuration(61)).toBe("2m")
112
+ })
113
+ })
114
+
115
+ describe("input validation", () => {
116
+ test("rejects invalid dates", () => {
117
+ expect(() => {
118
+ getPricingSnapshot(new Date("invalid"))
119
+ }).toThrow("Invalid date")
120
+ })
121
+ })
package/src/pricing.ts ADDED
@@ -0,0 +1,98 @@
1
+ export type PricingState = "peak" | "off-peak"
2
+ export type PricingReason =
3
+ | "peak-window"
4
+ | "outside-peak-window"
5
+ | "weekend"
6
+ | "chinese-public-holiday"
7
+
8
+ export interface PricingSnapshot {
9
+ state: PricingState
10
+ rateMultiplier: 1 | 0.5
11
+ reason: PricingReason
12
+ now: string
13
+ nextTransition: string
14
+ nextState: PricingState
15
+ secondsUntilTransition: number
16
+ holidayCalendarCovered: boolean
17
+ chinaDate: string
18
+ }
19
+
20
+ export interface PricingOptions {
21
+ chinesePublicHolidays?: ReadonlySet<string>
22
+ coveredHolidayYears?: ReadonlySet<number>
23
+ }
24
+
25
+ const PEAK_WINDOWS = [
26
+ { startHour: 1, endHour: 4 },
27
+ { startHour: 6, endHour: 10 },
28
+ ] as const
29
+ const CHINA_DATE_FORMATTER = new Intl.DateTimeFormat("en-CA", {
30
+ timeZone: "Asia/Shanghai",
31
+ year: "numeric",
32
+ month: "2-digit",
33
+ day: "2-digit",
34
+ })
35
+
36
+ function chinaDate(date: Date): string {
37
+ return CHINA_DATE_FORMATTER.format(date)
38
+ }
39
+ function isWeekday(date: Date): boolean {
40
+ const day = date.getUTCDay()
41
+ return day >= 1 && day <= 5
42
+ }
43
+ function isInsidePeakWindow(date: Date): boolean {
44
+ const hour = date.getUTCHours() + date.getUTCMinutes() / 60 + date.getUTCSeconds() / 3600
45
+ return PEAK_WINDOWS.some(({ startHour, endHour }) => hour >= startHour && hour < endHour)
46
+ }
47
+ function classify(
48
+ date: Date,
49
+ holidays: ReadonlySet<string>,
50
+ ): { state: PricingState; reason: PricingReason } {
51
+ if (holidays.has(chinaDate(date))) return { state: "off-peak", reason: "chinese-public-holiday" }
52
+ if (!isWeekday(date)) return { state: "off-peak", reason: "weekend" }
53
+ if (isInsidePeakWindow(date)) return { state: "peak", reason: "peak-window" }
54
+ return { state: "off-peak", reason: "outside-peak-window" }
55
+ }
56
+ function nextTransition(date: Date, holidays: ReadonlySet<string>): Date {
57
+ const current = classify(date, holidays).state
58
+ const cursor = new Date(date)
59
+ cursor.setUTCMilliseconds(0)
60
+ cursor.setUTCSeconds(0)
61
+ cursor.setUTCMinutes(0)
62
+ if (cursor.getTime() <= date.getTime()) cursor.setUTCHours(cursor.getUTCHours() + 1)
63
+ for (let i = 0; i < 24 * 14; i += 1) {
64
+ if (classify(cursor, holidays).state !== current) return cursor
65
+ cursor.setUTCHours(cursor.getUTCHours() + 1)
66
+ }
67
+ throw new Error("Unable to find the next DeepSeek pricing transition within 14 days")
68
+ }
69
+ export function getPricingSnapshot(
70
+ date = new Date(),
71
+ options: PricingOptions = {},
72
+ ): PricingSnapshot {
73
+ if (Number.isNaN(date.getTime())) throw new TypeError("Invalid date")
74
+ const holidays = options.chinesePublicHolidays ?? new Set<string>()
75
+ const coveredYears = options.coveredHolidayYears ?? new Set<number>()
76
+ const current = classify(date, holidays)
77
+ const transition = nextTransition(date, holidays)
78
+ const chinaDay = chinaDate(date)
79
+ return {
80
+ state: current.state,
81
+ rateMultiplier: current.state === "peak" ? 1 : 0.5,
82
+ reason: current.reason,
83
+ now: date.toISOString(),
84
+ nextTransition: transition.toISOString(),
85
+ nextState: current.state === "peak" ? "off-peak" : "peak",
86
+ secondsUntilTransition: Math.max(0, Math.ceil((transition.getTime() - date.getTime()) / 1000)),
87
+ holidayCalendarCovered: coveredYears.has(Number(chinaDay.slice(0, 4))),
88
+ chinaDate: chinaDay,
89
+ }
90
+ }
91
+ export function formatDuration(totalSeconds: number): string {
92
+ const minutes = Math.max(0, Math.ceil(totalSeconds / 60))
93
+ const hours = Math.floor(minutes / 60)
94
+ const rest = minutes % 60
95
+ if (hours === 0) return `${rest}m`
96
+ if (rest === 0) return `${hours}h`
97
+ return `${hours}h ${rest}m`
98
+ }
package/src/tui.tsx ADDED
@@ -0,0 +1,235 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+
3
+ import { Plugin } from "@opencode/plugin/tui"
4
+ import { createSignal } from "solid-js"
5
+ import { CHINESE_PUBLIC_HOLIDAYS, COVERED_HOLIDAY_YEARS } from "./holidays.js"
6
+ import {
7
+ formatDuration,
8
+ getPricingSnapshot,
9
+ type PricingReason,
10
+ type PricingSnapshot,
11
+ type PricingState,
12
+ } from "./pricing.js"
13
+
14
+ function createSnapshot(): PricingSnapshot {
15
+ return getPricingSnapshot(new Date(), {
16
+ chinesePublicHolidays: CHINESE_PUBLIC_HOLIDAYS,
17
+ coveredHolidayYears: COVERED_HOLIDAY_YEARS,
18
+ })
19
+ }
20
+
21
+ function stateLabel(state: PricingState): string {
22
+ return state === "peak" ? "🐳 peak" : "🐋💤 off-peak"
23
+ }
24
+
25
+ function stateName(state: PricingState): string {
26
+ return state === "peak" ? "peak" : "off-peak"
27
+ }
28
+
29
+ function StateLabel(props: { state: PricingState }) {
30
+ if (props.state === "peak") {
31
+ return <text>🐳 peak</text>
32
+ }
33
+
34
+ return (
35
+ <box flexDirection="row">
36
+ <text>🐋💤 </text>
37
+ <text fg="gray">off-peak</text>
38
+ </box>
39
+ )
40
+ }
41
+
42
+ function reasonLabel(reason: PricingReason): string {
43
+ switch (reason) {
44
+ case "peak-window":
45
+ return "weekday peak window"
46
+ case "outside-peak-window":
47
+ return "outside peak windows"
48
+ case "weekend":
49
+ return "weekend"
50
+ case "chinese-public-holiday":
51
+ return "Chinese public holiday"
52
+ }
53
+ }
54
+
55
+ function formatLocalTransition(snapshot: PricingSnapshot): string {
56
+ return new Date(snapshot.nextTransition).toLocaleString()
57
+ }
58
+
59
+ function isDeepSeekSelected(
60
+ model:
61
+ | {
62
+ readonly providerID: string
63
+ readonly modelID: string
64
+ readonly variant?: string
65
+ }
66
+ | undefined,
67
+ ): boolean {
68
+ return model?.modelID.toLowerCase().includes("deepseek") ?? false
69
+ }
70
+
71
+ export default Plugin.define({
72
+ id: "oc-deepseek-pricing",
73
+
74
+ setup(ctx) {
75
+ const [snapshot, setSnapshot] = createSignal(createSnapshot())
76
+
77
+ let transitionTimer: ReturnType<typeof setTimeout> | undefined
78
+ let minuteTimer: ReturnType<typeof setInterval> | undefined
79
+
80
+ const refresh = (): PricingSnapshot => {
81
+ const updated = createSnapshot()
82
+ setSnapshot(updated)
83
+ return updated
84
+ }
85
+
86
+ const scheduleTransition = (): void => {
87
+ if (transitionTimer !== undefined) {
88
+ clearTimeout(transitionTimer)
89
+ }
90
+
91
+ const current = snapshot()
92
+ const delay = Math.max(1_000, current.secondsUntilTransition * 1_000 + 250)
93
+
94
+ transitionTimer = setTimeout(() => {
95
+ refresh()
96
+ scheduleTransition()
97
+ }, delay)
98
+ }
99
+
100
+ const showStatus = (): void => {
101
+ const current = refresh()
102
+
103
+ const lines = [
104
+ `Status ${stateLabel(current.state)}`,
105
+ `Reason ${reasonLabel(current.reason)}`,
106
+ `Next ${stateName(current.nextState)} in ${formatDuration(current.secondsUntilTransition)}`,
107
+ `At ${formatLocalTransition(current)}`,
108
+ `China ${current.chinaDate}`,
109
+ ]
110
+
111
+ if (!current.holidayCalendarCovered) {
112
+ lines.push("Warning: Chinese holiday calendar is not configured for this year")
113
+ }
114
+
115
+ ctx.ui.toast.show({
116
+ title: "DeepSeek pricing",
117
+ message: lines.join("\n"),
118
+ variant: current.state === "peak" ? "warning" : "success",
119
+ })
120
+ }
121
+
122
+ const removePromptFooter = ctx.ui.slot({
123
+ append: "prompt.footer.status",
124
+
125
+ render() {
126
+ const model = ctx.ui.model.current()
127
+
128
+ if (!isDeepSeekSelected(model)) {
129
+ return null
130
+ }
131
+
132
+ const current = snapshot()
133
+
134
+ return (
135
+ <box flexDirection="row">
136
+ <StateLabel state={current.state} />
137
+ <text>
138
+ {" · "}
139
+ {formatDuration(current.secondsUntilTransition)} left
140
+ </text>
141
+ </box>
142
+ )
143
+ },
144
+ })
145
+
146
+ const removeHomeFooter = ctx.ui.slot({
147
+ append: "home.footer.status",
148
+
149
+ render() {
150
+ const model = ctx.ui.model.current()
151
+
152
+ if (!isDeepSeekSelected(model)) {
153
+ return null
154
+ }
155
+
156
+ const current = snapshot()
157
+
158
+ return (
159
+ <box flexDirection="row">
160
+ <StateLabel state={current.state} />
161
+ <text>
162
+ {" · "}
163
+ {formatDuration(current.secondsUntilTransition)} left
164
+ </text>
165
+ </box>
166
+ )
167
+ },
168
+ })
169
+
170
+ const removeSidebar = ctx.ui.slot({
171
+ append: "sidebar.content",
172
+
173
+ render() {
174
+ const model = ctx.ui.model.current()
175
+
176
+ if (!isDeepSeekSelected(model)) {
177
+ return null
178
+ }
179
+
180
+ const current = snapshot()
181
+
182
+ return (
183
+ <box flexDirection="column">
184
+ <box flexDirection="row">
185
+ <text>Status </text>
186
+ <StateLabel state={current.state} />
187
+ </box>
188
+ <text>{formatDuration(current.secondsUntilTransition)} left</text>
189
+ </box>
190
+ )
191
+ },
192
+ })
193
+
194
+ ctx.keymap.layer(() => ({
195
+ mode: "global",
196
+ commands: [
197
+ {
198
+ id: "deepseek-pricing.status",
199
+ title: "DeepSeek pricing status",
200
+ description: "Show the current DeepSeek peak or off-peak pricing window",
201
+ group: "DeepSeek",
202
+ enabled: () => isDeepSeekSelected(ctx.ui.model.current()),
203
+ palette: true,
204
+ slash: {
205
+ name: "deepseek-pricing",
206
+ aliases: ["deepseek-price"],
207
+ },
208
+ run: async () => {
209
+ showStatus()
210
+ },
211
+ },
212
+ ],
213
+ }))
214
+
215
+ minuteTimer = setInterval(() => {
216
+ refresh()
217
+ }, 60_000)
218
+
219
+ scheduleTransition()
220
+
221
+ return () => {
222
+ if (transitionTimer !== undefined) {
223
+ clearTimeout(transitionTimer)
224
+ }
225
+
226
+ if (minuteTimer !== undefined) {
227
+ clearInterval(minuteTimer)
228
+ }
229
+
230
+ removePromptFooter()
231
+ removeHomeFooter()
232
+ removeSidebar()
233
+ }
234
+ },
235
+ })