pi-deepseek-tariff 0.3.1 → 0.3.3
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/README.md +12 -10
- package/extensions/deepseek-peak.ts +27 -1
- package/package.json +1 -1
- package/src/i18n.ts +13 -8
- package/src/view.ts +36 -9
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A [pi](https://pi.dev) extension that shows the current **DeepSeek API tariff wi
|
|
|
10
10
|
DeepSeek charges double during peak hours, so knowing the current window is worth real money.
|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
|
|
13
|
+
◷ deepseek OFF-PEAK → 11h 59m ← footer line, always visible
|
|
14
14
|
|
|
15
15
|
Now OFF-PEAK
|
|
16
16
|
Computer: 14:43:52 26-10-2026(MSK) | UTC: 2026-10-26 11:43
|
|
@@ -38,7 +38,7 @@ Commands: /dsp on|off|refresh|en|ru · /dsp footer on|off
|
|
|
38
38
|
pi install npm:pi-deepseek-tariff
|
|
39
39
|
|
|
40
40
|
# from git: tags mirror the npm versions, so pin whichever you want
|
|
41
|
-
pi install git:github.com/valderan/pi-deepseek-tariff@v0.3.
|
|
41
|
+
pi install git:github.com/valderan/pi-deepseek-tariff@v0.3.3
|
|
42
42
|
|
|
43
43
|
# local checkout, for development
|
|
44
44
|
git clone git@github.com:valderan/pi-deepseek-tariff.git
|
|
@@ -62,16 +62,18 @@ Then run `/dsp`. Afterwards `pi list`, `pi update --extensions`, `pi remove …`
|
|
|
62
62
|
| `/dsp en` / `ru` | switch the panel language (persisted) |
|
|
63
63
|
| `/dsp footer on` / `off` (or `/dsp footeron` / `footeroff`) | show or hide the footer line (persisted) |
|
|
64
64
|
|
|
65
|
+
Type `/dsp ` and press Tab: the arguments above are offered as completions, with a short description each (`/dsp footer <Tab>` completes `on` and `off`).
|
|
66
|
+
|
|
65
67
|
## What it does
|
|
66
68
|
|
|
67
|
-
- **Footer line** — one compact status line:
|
|
69
|
+
- **Footer line** — one compact status line: `◷ deepseek OFF-PEAK → 11h 59m`, or `◷ deepseek PEAK → 1h 40m` during peak. The clock glyph leads the line and takes the state colour — green for off-peak, red for peak — and the tariff word next to it is coloured the same way. The brand is written lowercase in DeepSeek blue (`#4175E6`), the countdown uses the theme accent. Shown only while a DeepSeek model is active; `/dsp footer off` hides it for good.
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
The tariff is spelled `PEAK` / `OFF-PEAK` in English regardless of the panel language; only the countdown units are localized, so Russian reads `◷ deepseek OFF-PEAK → 11ч 59м`.
|
|
70
72
|
|
|
71
73
|
Pi renders every extension status on a single footer line, joined alphabetically, so the segment is kept short.
|
|
72
74
|
- **Peak rules** — DeepSeek defines peak as `01:00-04:00` and `06:00-10:00` **UTC, Monday to Friday**; weekends and Chinese public holidays are off-peak all day.
|
|
73
75
|
- **Local time everywhere** — the UTC clock comes from the machine clock, so peak windows, weekday labels and countdowns are rendered in *your* zone (`(MSK)`, `(UTC-04:00)`, …). Weekday labels shift with the zone: at UTC−4 the windows land on Sun–Thu.
|
|
74
|
-
- **Prices** — parsed from `api-docs.deepseek.com/quick_start/pricing/`: cache hit, cache miss and output per model, plus the Search & Fetch request rates. The tariff in effect is highlighted.
|
|
76
|
+
- **Prices** — parsed from `api-docs.deepseek.com/quick_start/pricing/`: cache hit, cache miss and output per model, plus the Search & Fetch request rates. The tariff in effect is highlighted, and the `DeepSeek` word in the `DeepSeek prices:` label keeps the same brand blue as the footer.
|
|
75
77
|
- **Caching** — the page is parsed at most once per `pricingTtlHours` (default 6), and the result is cached on disk, so restarts stay offline. `PI_OFFLINE` disables network access entirely and the panel falls back to built-in values.
|
|
76
78
|
|
|
77
79
|
## Configuration
|
|
@@ -134,11 +136,11 @@ the commit whose `package.json` says `X.Y.Z`, and npm publishes that same versio
|
|
|
134
136
|
|
|
135
137
|
```bash
|
|
136
138
|
# 1. bump package.json, add a CHANGELOG entry, commit
|
|
137
|
-
npm version 0.3.
|
|
138
|
-
git commit -am "chore: release 0.3.
|
|
139
|
+
npm version 0.3.3 --no-git-tag-version # or edit by hand
|
|
140
|
+
git commit -am "chore: release 0.3.3"
|
|
139
141
|
|
|
140
142
|
# 2. tag the release commit and push
|
|
141
|
-
git tag -a v0.3.
|
|
143
|
+
git tag -a v0.3.3 -m v0.3.3
|
|
142
144
|
git push origin main --tags
|
|
143
145
|
|
|
144
146
|
# 3. publish the tag to npm (2FA prompt, or --otp=123456)
|
|
@@ -149,7 +151,7 @@ npm publish --access public
|
|
|
149
151
|
Check the result:
|
|
150
152
|
|
|
151
153
|
```bash
|
|
152
|
-
npm view pi-deepseek-tariff dist-tags # { latest: '0.3.
|
|
154
|
+
npm view pi-deepseek-tariff dist-tags # { latest: '0.3.3' }
|
|
153
155
|
npm view pi-deepseek-tariff versions
|
|
154
156
|
```
|
|
155
157
|
|
|
@@ -157,7 +159,7 @@ npm view pi-deepseek-tariff versions
|
|
|
157
159
|
`NPM_TOKEN` secret is configured; with trusted publishing enabled on npmjs.com the
|
|
158
160
|
token is not needed at all and the build is signed with provenance.
|
|
159
161
|
|
|
160
|
-
Tags are pinnable with `pi install git:…@v0.3.
|
|
162
|
+
Tags are pinnable with `pi install git:…@v0.3.3`. Published versions show up in the
|
|
161
163
|
[pi package gallery](https://pi.dev/packages) through the `pi-package` keyword.
|
|
162
164
|
|
|
163
165
|
## Changelog
|
|
@@ -166,11 +166,37 @@ export default function deepseekPeak(pi: ExtensionAPI) {
|
|
|
166
166
|
render(ctx);
|
|
167
167
|
};
|
|
168
168
|
|
|
169
|
+
// Tab completion for the documented arguments, so `/dsp <TAB>` lists what the
|
|
170
|
+
// handler actually accepts instead of leaving them buried in the README.
|
|
171
|
+
const ARGUMENTS = [
|
|
172
|
+
{ value: "on", label: "on", description: "Show the panel · Показать панель" },
|
|
173
|
+
{ value: "off", label: "off", description: "Hide the panel · Скрыть панель" },
|
|
174
|
+
{ value: "refresh", label: "refresh", description: "Re-parse prices now · Обновить цены сейчас" },
|
|
175
|
+
{ value: "en", label: "en", description: "Panel language: English · Язык панели: английский" },
|
|
176
|
+
{ value: "ru", label: "ru", description: "Panel language: Russian · Язык панели: русский" },
|
|
177
|
+
{ value: "footer", label: "footer", description: "Footer line: on|off · Строка футера: on|off" },
|
|
178
|
+
];
|
|
179
|
+
const FOOTER_ARGUMENTS = ARGUMENTS.filter((item) => item.value === "on" || item.value === "off");
|
|
180
|
+
|
|
181
|
+
const getArgumentCompletions = (prefix: string) => {
|
|
182
|
+
const words = prefix.trimStart().split(/\s+/);
|
|
183
|
+
if (words.length > 2) return null;
|
|
184
|
+
const [head, tail] = words;
|
|
185
|
+
if (head === undefined || head === "") return ARGUMENTS;
|
|
186
|
+
if (tail === undefined) {
|
|
187
|
+
const matches = ARGUMENTS.filter((item) => item.value.startsWith(head.toLowerCase()));
|
|
188
|
+
return matches.length > 0 ? matches : null;
|
|
189
|
+
}
|
|
190
|
+
if (head.toLowerCase() !== "footer") return null;
|
|
191
|
+
const matches = FOOTER_ARGUMENTS.filter((item) => item.value.startsWith(tail.toLowerCase()));
|
|
192
|
+
return matches.length > 0 ? matches : null;
|
|
193
|
+
};
|
|
194
|
+
|
|
169
195
|
const description = "DeepSeek peak/off-peak panel · Панель DeepSeek peak/off-peak";
|
|
170
196
|
// `/dsp` is the short form; `deepseek-peek` accepts the spelling people reach
|
|
171
197
|
// for when they type "peak" wrong, so the command still resolves.
|
|
172
198
|
for (const name of ["deepseek-peak", "dsp", "deepseek-peek"]) {
|
|
173
|
-
pi.registerCommand(name, { description, handler: commandHandler });
|
|
199
|
+
pi.registerCommand(name, { description, handler: commandHandler, getArgumentCompletions });
|
|
174
200
|
}
|
|
175
201
|
}
|
|
176
202
|
|
package/package.json
CHANGED
package/src/i18n.ts
CHANGED
|
@@ -23,6 +23,8 @@ type Strings = {
|
|
|
23
23
|
nextIn: (duration: string) => string;
|
|
24
24
|
and: string;
|
|
25
25
|
prices: string;
|
|
26
|
+
/** The product name inside `prices`, rendered in the DeepSeek blue instead of the label token. */
|
|
27
|
+
pricesBrand: string;
|
|
26
28
|
pricesState: (state: string) => string;
|
|
27
29
|
activeModel: string;
|
|
28
30
|
peakWindows: (zone: string) => string;
|
|
@@ -34,6 +36,7 @@ type Strings = {
|
|
|
34
36
|
sourceBuiltin: string;
|
|
35
37
|
requestPricing: string;
|
|
36
38
|
commands: string;
|
|
39
|
+
/** Footer line — the state is always written in English (PEAK / OFF-PEAK), in every language. */
|
|
37
40
|
statusBrand: string;
|
|
38
41
|
statusPeak: string;
|
|
39
42
|
statusOffPeak: string;
|
|
@@ -53,6 +56,7 @@ export const STRINGS: Record<Lang, Strings> = {
|
|
|
53
56
|
nextIn: (duration) => `in ${duration}`,
|
|
54
57
|
and: "and",
|
|
55
58
|
prices: "DeepSeek prices:",
|
|
59
|
+
pricesBrand: "DeepSeek",
|
|
56
60
|
pricesState: (state) => `$ per 1M tokens, active: ${state}`,
|
|
57
61
|
activeModel: "← active",
|
|
58
62
|
peakWindows: (zone) => `Peak windows (${zone}):`,
|
|
@@ -64,10 +68,10 @@ export const STRINGS: Record<Lang, Strings> = {
|
|
|
64
68
|
sourceBuiltin: "built-in values (page unavailable)",
|
|
65
69
|
requestPricing: "Search & Fetch:",
|
|
66
70
|
commands: "Commands:",
|
|
67
|
-
statusBrand: "
|
|
68
|
-
statusPeak: "
|
|
69
|
-
statusOffPeak: "
|
|
70
|
-
statusFor: (duration) =>
|
|
71
|
+
statusBrand: "deepseek",
|
|
72
|
+
statusPeak: "PEAK",
|
|
73
|
+
statusOffPeak: "OFF-PEAK",
|
|
74
|
+
statusFor: (duration) => `\u2192 ${duration}`,
|
|
71
75
|
locale: "en-GB",
|
|
72
76
|
hourUnit: "h",
|
|
73
77
|
minuteUnit: "m",
|
|
@@ -81,6 +85,7 @@ export const STRINGS: Record<Lang, Strings> = {
|
|
|
81
85
|
nextIn: (duration) => `через ${duration}`,
|
|
82
86
|
and: "и",
|
|
83
87
|
prices: "Цены DeepSeek:",
|
|
88
|
+
pricesBrand: "DeepSeek",
|
|
84
89
|
pricesState: (state) => `$ за 1M токенов, активен ${state}`,
|
|
85
90
|
activeModel: "← активна",
|
|
86
91
|
peakWindows: (zone) => `Окна peak (${zone}):`,
|
|
@@ -92,10 +97,10 @@ export const STRINGS: Record<Lang, Strings> = {
|
|
|
92
97
|
sourceBuiltin: "встроенные значения (страница недоступна)",
|
|
93
98
|
requestPricing: "Search & Fetch:",
|
|
94
99
|
commands: "Команды:",
|
|
95
|
-
statusBrand: "
|
|
96
|
-
statusPeak: "
|
|
97
|
-
statusOffPeak: "
|
|
98
|
-
statusFor: (duration) =>
|
|
100
|
+
statusBrand: "deepseek",
|
|
101
|
+
statusPeak: "PEAK",
|
|
102
|
+
statusOffPeak: "OFF-PEAK",
|
|
103
|
+
statusFor: (duration) => `\u2192 ${duration}`,
|
|
99
104
|
locale: "ru-RU",
|
|
100
105
|
hourUnit: "ч",
|
|
101
106
|
minuteUnit: "м",
|
package/src/view.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Presentation: the footer status line and the panel body.
|
|
3
3
|
*
|
|
4
|
-
* Colors follow the pi theme:
|
|
5
|
-
* token,
|
|
4
|
+
* Colors follow the pi theme: the footer clock and the tariff word share the
|
|
5
|
+
* state token (green off-peak, red peak), the countdown uses the accent token,
|
|
6
|
+
* and only the brand keeps a fixed colour. The panel below the editor keeps the
|
|
7
|
+
* warning/text pairing.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { parseColor } from "@earendil-works/pi-tui";
|
|
9
12
|
import type { Config } from "./config.ts";
|
|
10
13
|
import { localizeUnit, STRINGS, type Lang } from "./i18n.ts";
|
|
11
14
|
import type { Pricing, RequestPricing } from "./pricing.ts";
|
|
@@ -26,6 +29,12 @@ import {
|
|
|
26
29
|
/** Placeholder for a full-width rule; the renderer expands it to the panel width. */
|
|
27
30
|
export const SEPARATOR = "\u0000rule";
|
|
28
31
|
|
|
32
|
+
/** Footer clock: one cell wide, so the segment stays short on the shared status line. */
|
|
33
|
+
const CLOCK = "\u25F7"; // ◷
|
|
34
|
+
|
|
35
|
+
/** DeepSeek brand blue. Fixed in every theme: it is the product colour, not a theme role. */
|
|
36
|
+
const BRAND_BLUE = parseColor("#4175E6");
|
|
37
|
+
|
|
29
38
|
/** "Search & Fetch" value: a shared price is printed once. */
|
|
30
39
|
export function requestPricingText(entries: RequestPricing[], lang: Lang): string | undefined {
|
|
31
40
|
if (entries.length === 0) return undefined;
|
|
@@ -54,9 +63,12 @@ export function requestPricingText(entries: RequestPricing[], lang: Lang): strin
|
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
/**
|
|
57
|
-
* Compact footer line: a coloured
|
|
58
|
-
*
|
|
59
|
-
*
|
|
66
|
+
* Compact footer line: a state-coloured clock, the brand name in DeepSeek blue,
|
|
67
|
+
* the tariff in English and the countdown in the theme accent — for example
|
|
68
|
+
* "◷ deepseek OFF-PEAK → 11h 59m" or "◷ deepseek PEAK → 1h 40m".
|
|
69
|
+
*
|
|
70
|
+
* The clock carries the state colour, so the line stays legible without a
|
|
71
|
+
* separate status dot; the duration keeps the panel language units.
|
|
60
72
|
*/
|
|
61
73
|
export function statusText(theme: Theme, config: Config, lang: Lang, pricing: Pricing): string {
|
|
62
74
|
void pricing;
|
|
@@ -64,10 +76,25 @@ export function statusText(theme: Theme, config: Config, lang: Lang, pricing: Pr
|
|
|
64
76
|
const now = new Date();
|
|
65
77
|
const peak = isPeak(now, config.holidays);
|
|
66
78
|
const next = nextTransition(now, config.holidays);
|
|
67
|
-
const
|
|
68
|
-
const label = peak ? text.statusPeak : text.statusOffPeak;
|
|
79
|
+
const state = peak ? "error" : "success";
|
|
69
80
|
const duration = humanize(next.getTime() - now.getTime(), lang);
|
|
70
|
-
|
|
81
|
+
const clock = theme.fg(state, CLOCK);
|
|
82
|
+
const brand = theme.style(text.statusBrand, { fg: BRAND_BLUE });
|
|
83
|
+
const label = theme.fg(state, peak ? text.statusPeak : text.statusOffPeak);
|
|
84
|
+
return `${clock} ${brand} ${label} ${theme.fg("accent", text.statusFor(duration))}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Colour the product name inside a panel label without splitting the string in
|
|
89
|
+
* i18n: the label stays one translatable line, only the brand word is swapped to
|
|
90
|
+
* the DeepSeek blue. Falls back to `rest(label)` when the name is missing.
|
|
91
|
+
*/
|
|
92
|
+
function brandLabel(theme: Theme, label: string, brand: string, rest: (value: string) => string): string {
|
|
93
|
+
const at = label.indexOf(brand);
|
|
94
|
+
if (at < 0) return rest(label);
|
|
95
|
+
const head = rest(label.slice(0, at));
|
|
96
|
+
const tail = rest(label.slice(at + brand.length));
|
|
97
|
+
return `${head}${theme.style(brand, { fg: BRAND_BLUE })}${tail}`;
|
|
71
98
|
}
|
|
72
99
|
|
|
73
100
|
export function panelLines(
|
|
@@ -111,7 +138,7 @@ export function panelLines(
|
|
|
111
138
|
);
|
|
112
139
|
|
|
113
140
|
// 4. Model prices with the active tariff highlighted.
|
|
114
|
-
lines.push(`${
|
|
141
|
+
lines.push(`${brandLabel(theme, text.prices, text.pricesBrand, yellow)} ${white(text.pricesState(state))}`);
|
|
115
142
|
const value = (amount: number) => amount.toFixed(4).padStart(8);
|
|
116
143
|
for (const [name, model] of Object.entries(pricing.models)) {
|
|
117
144
|
const id = `${config.providerPrefix}${name}`;
|