oc-deepseek-pricing 0.0.0-stage → 0.1.1
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 +21 -0
- package/README.md +211 -2
- package/package.json +55 -4
- package/src/holidays.ts +56 -0
- package/src/pricing.ts +98 -0
- package/src/tui.tsx +235 -0
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,212 @@
|
|
|
1
|
-
#
|
|
1
|
+
# oc-deepseek-pricing
|
|
2
2
|
|
|
3
|
-
|
|
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 shown when the selected model ID contains `deepseek`. 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 a compact pricing status in the session sidebar.
|
|
11
|
+
- Exposes complete pricing details through `/deepseek-pricing`.
|
|
12
|
+
- Distinguishes peak and off-peak periods with whale emoji indicators.
|
|
13
|
+
- Shows the next pricing transition and a countdown.
|
|
14
|
+
- Treats weekends as fully off-peak.
|
|
15
|
+
- Supports Chinese public-holiday overrides.
|
|
16
|
+
- Warns when the holiday calendar does not cover the current Chinese calendar year.
|
|
17
|
+
- Uses local computation only; no network request is needed at runtime.
|
|
18
|
+
|
|
19
|
+
## Pricing schedule
|
|
20
|
+
|
|
21
|
+
Peak windows are evaluated in UTC:
|
|
22
|
+
|
|
23
|
+
- 01:00-04:00 UTC, Monday through Friday;
|
|
24
|
+
- 06:00-10:00 UTC, Monday through Friday.
|
|
25
|
+
|
|
26
|
+
All other times are off-peak, including:
|
|
27
|
+
|
|
28
|
+
- 04:00-06:00 UTC on weekdays;
|
|
29
|
+
- all hours after 10:00 UTC and before 01:00 UTC;
|
|
30
|
+
- Saturdays and Sundays in full;
|
|
31
|
+
- Chinese public holidays in full.
|
|
32
|
+
|
|
33
|
+
The off-peak price multiplier is 0.5.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
Install the published plugin directly with the OpenCode CLI:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
opencode plugin add oc-deepseek-pricing
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
To install this exact release:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
opencode plugin add oc-deepseek-pricing@0.1.0
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
OpenCode registers the plugin automatically. No manual change to `cli.json`, `opencode.json`, or `tui.jsonc` is required.
|
|
50
|
+
|
|
51
|
+
Verify the installation with:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
opencode plugin list
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- OpenCode v2 with TUI plugin support;
|
|
60
|
+
- a terminal supported by OpenCode.
|
|
61
|
+
|
|
62
|
+
Development dependencies include:
|
|
63
|
+
|
|
64
|
+
- `@opencode/plugin`;
|
|
65
|
+
- `@opentui/core`;
|
|
66
|
+
- `@opentui/solid`;
|
|
67
|
+
- `solid-js`;
|
|
68
|
+
- TypeScript.
|
|
69
|
+
|
|
70
|
+
## Repository structure
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
oc-deepseek-pricing/
|
|
74
|
+
├── CALENDAR_UPDATE.md
|
|
75
|
+
├── LICENSE
|
|
76
|
+
├── README.md
|
|
77
|
+
├── bun.lock
|
|
78
|
+
├── package.json
|
|
79
|
+
├── src/
|
|
80
|
+
│ ├── holidays.ts
|
|
81
|
+
│ ├── pricing.test.ts
|
|
82
|
+
│ ├── pricing.ts
|
|
83
|
+
│ └── tui.tsx
|
|
84
|
+
├── tsconfig.json
|
|
85
|
+
└── tui.ts
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Development setup
|
|
89
|
+
|
|
90
|
+
Install dependencies:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
bun install
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Run all validation checks:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
bun run check
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Run the test suite:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
bun test
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A successful validation completes without formatting, lint, TypeScript, or test errors.
|
|
109
|
+
|
|
110
|
+
## Restarting OpenCode
|
|
111
|
+
|
|
112
|
+
After changing the plugin source or configuration, close the OpenCode TUI completely and start it again.
|
|
113
|
+
|
|
114
|
+
Opening a new conversation is not necessarily sufficient because plugin entrypoints are loaded during TUI startup.
|
|
115
|
+
|
|
116
|
+
## Expected interface
|
|
117
|
+
|
|
118
|
+
When a DeepSeek model is selected during an off-peak window, the prompt and home footers display:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
🐋💤 off-peak · 9h 13m left
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
During a peak window, they display:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
🐳 peak · 1h 15m left
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The session sidebar displays only the current status and remaining time:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
Status 🐋💤 off-peak
|
|
134
|
+
9h 13m left
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
During a peak window, the sidebar displays:
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
Status 🐳 peak
|
|
141
|
+
1h 15m left
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Run `/deepseek-pricing` to display the complete pricing details:
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
DeepSeek pricing
|
|
148
|
+
Status 🐋💤 off-peak
|
|
149
|
+
Reason outside peak windows
|
|
150
|
+
Next peak in 9h 13m
|
|
151
|
+
At 10/28/2026, 2:00:00 AM
|
|
152
|
+
China 2026-10-27
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The `Next` line intentionally uses the state name without an emoji. If the Chinese public-holiday calendar does not cover the current year, the command also displays a warning.
|
|
156
|
+
|
|
157
|
+
The exact local transition time depends on the operating-system timezone and daylight-saving rules. Pricing classification is based on UTC, while holiday lookup uses the Asia/Shanghai calendar date.
|
|
158
|
+
|
|
159
|
+
## Chinese public holidays
|
|
160
|
+
|
|
161
|
+
Holiday data is stored in:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
src/holidays.ts
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Only officially verified calendar years should be added to `COVERED_HOLIDAY_YEARS`.
|
|
168
|
+
|
|
169
|
+
For the complete annual update procedure, source requirements, timezone rules, validation checklist, and required tests, see [CALENDAR_UPDATE.md](./CALENDAR_UPDATE.md).
|
|
170
|
+
|
|
171
|
+
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.
|
|
172
|
+
|
|
173
|
+
## Tests
|
|
174
|
+
|
|
175
|
+
The test suite covers:
|
|
176
|
+
|
|
177
|
+
- the first weekday peak window;
|
|
178
|
+
- the off-peak gap between the two peak windows;
|
|
179
|
+
- the second weekday peak window;
|
|
180
|
+
- exact peak-window boundaries;
|
|
181
|
+
- weekends;
|
|
182
|
+
- ordinary weekday off-peak periods;
|
|
183
|
+
- Chinese public-holiday overrides;
|
|
184
|
+
- calendar-year coverage;
|
|
185
|
+
- next-transition calculation;
|
|
186
|
+
- weekend skipping when finding the next peak period;
|
|
187
|
+
- duration formatting;
|
|
188
|
+
- invalid date handling.
|
|
189
|
+
|
|
190
|
+
Run:
|
|
191
|
+
|
|
192
|
+
```sh
|
|
193
|
+
bun test
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Type checking
|
|
197
|
+
|
|
198
|
+
Run:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
bun run typecheck
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The repository uses NodeNext module resolution. Relative TypeScript imports must therefore use the emitted JavaScript extension, for example:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { getPricingSnapshot } from "./pricing.js"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
This project is licensed under the MIT License. See [LICENSE](./LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oc-deepseek-pricing",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
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/holidays.ts",
|
|
16
|
+
"src/pricing.ts",
|
|
17
|
+
"src/tui.tsx",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
"./tui": "./src/tui.tsx"
|
|
23
|
+
},
|
|
24
|
+
"opencode": {
|
|
25
|
+
"tui": "./src/tui.tsx"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "bun test",
|
|
29
|
+
"check": "bun run format:check && bun run lint && bun run typecheck && bun test",
|
|
30
|
+
"format": "biome format --write .",
|
|
31
|
+
"format:check": "biome format .",
|
|
32
|
+
"lint": "biome lint .",
|
|
33
|
+
"lint:fix": "biome lint --write .",
|
|
34
|
+
"biome:check": "biome check .",
|
|
35
|
+
"biome:fix": "biome check --write .",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"pack:check": "npm pack --dry-run",
|
|
38
|
+
"prepublishOnly": "bun run check"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@biomejs/biome": "2.6.3",
|
|
42
|
+
"@opencode/plugin": "2.2.0",
|
|
43
|
+
"@opentui/core": "0.7.0",
|
|
44
|
+
"@opentui/solid": "0.7.0",
|
|
45
|
+
"@types/bun": "1.4.4",
|
|
46
|
+
"solid-js": "1.9.17",
|
|
47
|
+
"typescript": "5.9.3"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/scicco/oc-deepseek-pricing.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/scicco/oc-deepseek-pricing/issues"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/scicco/oc-deepseek-pricing#readme"
|
|
57
|
+
}
|
package/src/holidays.ts
ADDED
|
@@ -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])
|
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
|
+
})
|