coding-usage-bar 0.5.1 → 0.6.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/README.md CHANGED
@@ -114,6 +114,8 @@ Coding Usage Bar calls the Zhipu AI quota API (`GET /api/monitor/usage/quota/lim
114
114
 
115
115
  Get your API key from the [Zhipu AI console](https://open.bigmodel.cn). Without this key, GLM monitoring will report `GLM_API_KEY_MISSING`.
116
116
 
117
+ Zhipu meters newer Coding Plans in credits (积分) instead of raw tokens and reports them as `CREDIT_LIMIT` windows. Coding Usage Bar keys off the response shape, records `quotaUnit` as `credits` or `tokens` in `status.json`, and labels the dropdown with `metered in credits` so the 5h/7d percentages read correctly for either plan generation. This detection has not been verified against a real V3 (points-based) account yet; it is based on the `CREDIT_LIMIT` shape observed on existing plans. If you are on a V3 plan, a redacted `coding-usage-bar status --refresh` output in an issue would confirm it.
118
+
117
119
  ## DeepSeek
118
120
 
119
121
  Coding Usage Bar calls `GET https://api.deepseek.com/user/balance` to read your account balance. DeepSeek exposes balance rather than 5h/7d usage windows, so the menu bar shows `Available` or `Depleted` plus the currency amount. Set `deepseek.apiKey` in `~/.coding-usage-bar/config.json`:
@@ -129,6 +131,17 @@ Coding Usage Bar calls `GET https://api.deepseek.com/user/balance` to read your
129
131
 
130
132
  Without this key, DeepSeek monitoring will report `DEEPSEEK_API_KEY_MISSING`.
131
133
 
134
+ ## Peak and off-peak pricing
135
+
136
+ GLM Coding Plan and the DeepSeek API both publish a clock-based discount, and both state it in Beijing time. Coding Usage Bar computes the current tier on the producer side, stores it in `status.json` as `pricing`, and shows it in the CLI table (`price` row), the dropdown, and the usage card, together with the time of the next flip:
137
+
138
+ | Provider | Peak (Asia/Shanghai) | Off-peak | Holidays |
139
+ |---|---|---|---|
140
+ | DeepSeek API | Mon–Fri 09:00–12:00 and 14:00–18:00 | 50% of the peak rate | Chinese public holidays are off-peak all day |
141
+ | GLM Coding Plan | Mon–Fri 14:00–18:00 | credits deducted at 50% | Not excluded by the published rule |
142
+
143
+ Sources: [DeepSeek pricing](https://api-docs.deepseek.com/quick_start/pricing) and the [GLM Coding Plan overview](https://docs.bigmodel.cn/cn/coding-plan/overview). The 2026 holiday table follows the State Council notice (国办发明电〔2025〕7号); adjusted working weekends stay off-peak because both vendors phrase the rule as Monday to Friday. Time-boxed promotions are not modelled. The tier only tells you what the provider charges right now; it does not change the burn analysis.
144
+
132
145
  ## MiniMax (M3)
133
146
 
134
147
  Coding Usage Bar calls `GET https://api.minimaxi.com/v1/token_plan/remains` to read 5h and 7d usage windows. MiniMax returns two signals per model: a count-based ratio (`current_*_usage_count` / `current_*_total_count`) for tiered models like `video`, and a credit-based `current_*_remaining_percent` (0-100) for the `general` model where the count stays at 0. Coding Usage Bar prefers the count ratio when total > 0 and otherwise derives used% as `100 - remaining_percent`. Set `minimax.apiKey` and (optionally) `minimax.region` in `~/.coding-usage-bar/config.json`:
package/README.zh-CN.md CHANGED
@@ -50,6 +50,17 @@ coding-usage-bar status
50
50
  - Claude Code、Codex 本地优先,不依赖额外遥测服务
51
51
  - 展示层只读稳定的 `status.json`,不会在打开菜单时临时访问数据源
52
52
  - 接近限额或燃烧节奏异常时发送系统通知
53
+ - 国产套餐规则内置:GLM Coding Plan 与 DeepSeek 的峰谷时段按北京时间计算,法定节假日按国办通知处理,CLI、下拉菜单和卡片都会显示当前是高峰还是谷时、几折、几点切换
54
+ - GLM 新版 Coding Plan 按积分(`CREDIT_LIMIT`)计量时自动识别,标注"按积分计量",5h/7d 百分比不会因为套餐换代而失真。该识别基于现有套餐观测到的 `CREDIT_LIMIT` 形状,尚未用真实 V3 积分制账号实测;V3 用户可在 issue 里贴脱敏的 `coding-usage-bar status --refresh` 输出帮助确认
55
+
56
+ ## 峰谷时段
57
+
58
+ | Provider | 高峰(北京时间) | 谷时 | 法定节假日 |
59
+ |---|---|---|---|
60
+ | DeepSeek API | 周一至周五 09:00–12:00、14:00–18:00 | 高峰价的 50% | 全天谷时 |
61
+ | GLM Coding Plan | 周一至周五 14:00–18:00 | 积分按 50% 抵扣 | 官方规则未排除,按工作日字面处理 |
62
+
63
+ 依据 [DeepSeek 定价页](https://api-docs.deepseek.com/quick_start/pricing)与 [GLM Coding Plan 概览](https://docs.bigmodel.cn/cn/coding-plan/overview)。2026 年节假日表来自国办发明电〔2025〕7号;调休上班的周末仍按周末处理,因为两家都把规则写成"周一至周五"。限时活动(如双节全天谷时)不建模。峰谷只说明厂商此刻怎么计费,不改变燃烧节奏分析。
53
64
 
54
65
  完整命令、Provider 配置和实现边界请查看[英文 README](README.md)。
55
66
 
package/dist/deepseek.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { deepseekPricing } from "./pricing.js";
1
2
  const DEFAULT_BASE_URL = "https://api.deepseek.com";
2
3
  export async function fetchDeepseekBalance(config) {
3
4
  const url = `${DEFAULT_BASE_URL}/user/balance`;
@@ -19,11 +20,13 @@ export function usageFromDeepseekBalance(balance, options) {
19
20
  if (!info) {
20
21
  return null;
21
22
  }
23
+ const observedAt = options.observedAt ?? new Date().toISOString();
22
24
  return {
23
25
  provider: "deepseek",
24
26
  source: options.source,
25
- observedAt: options.observedAt ?? new Date().toISOString(),
27
+ observedAt,
26
28
  planType: null,
29
+ pricing: deepseekPricing(new Date(observedAt)),
27
30
  windows: [],
28
31
  balance: {
29
32
  total: info.total_balance,
package/dist/format.js CHANGED
@@ -62,10 +62,24 @@ export function usageBar(usedPercent, width = 12) {
62
62
  const usedCells = Math.max(0, Math.min(width, Math.round((usedPercent / 100) * width)));
63
63
  return `[${"#".repeat(usedCells)}${"-".repeat(width - usedCells)}] ${Math.round(usedPercent)}%`;
64
64
  }
65
+ function pricingRow(usage) {
66
+ if (!usage.pricing) {
67
+ return null;
68
+ }
69
+ const tier = usage.pricing.tier === "off_peak" ? `off-peak ${usage.pricing.discountPercent}% off` : "peak, full price";
70
+ return [
71
+ providerLabel(usage.provider).padEnd(8),
72
+ "price".padEnd(8),
73
+ tier.padEnd(19),
74
+ formatDurationUntil(usage.pricing.until).padEnd(10),
75
+ usage.quotaUnit === "credits" ? "credits" : "",
76
+ ].join("");
77
+ }
65
78
  export function formatStatusRows(usages, analyses) {
66
79
  const rows = ["Provider Period Usage Reset State"];
67
80
  for (const usage of usages) {
68
81
  const analysis = analyses.find((item) => item.provider === usage.provider);
82
+ const price = pricingRow(usage);
69
83
  if (usage.balance && usage.windows.length === 0) {
70
84
  const currency = usage.balance.currency === "CNY" ? "¥" : "$";
71
85
  rows.push([
@@ -75,6 +89,8 @@ export function formatStatusRows(usages, analyses) {
75
89
  "".padEnd(10),
76
90
  analysis?.state ?? "RAW",
77
91
  ].join(""));
92
+ if (price)
93
+ rows.push(price);
78
94
  continue;
79
95
  }
80
96
  for (const window of usage.windows) {
@@ -86,6 +102,8 @@ export function formatStatusRows(usages, analyses) {
86
102
  analysis?.state ?? "RAW",
87
103
  ].join(""));
88
104
  }
105
+ if (price)
106
+ rows.push(price);
89
107
  }
90
108
  return rows.join("\n");
91
109
  }
package/dist/glm.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { makeProviderUsage, normalizeWindow } from "./usage.js";
2
+ import { glmPricing } from "./pricing.js";
2
3
  const DEFAULT_BASE_URL = "https://open.bigmodel.cn";
3
4
  export async function fetchGlmQuota(config) {
4
5
  const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
@@ -50,7 +51,7 @@ function pickTokenWindows(limits) {
50
51
  for (const type of ["TOKENS_LIMIT", "CREDIT_LIMIT"]) {
51
52
  const picked = pickTokenWindowsOfType(limits.filter((limit) => limit.type === type));
52
53
  if (picked) {
53
- return picked;
54
+ return { ...picked, unit: type === "CREDIT_LIMIT" ? "credits" : "tokens" };
54
55
  }
55
56
  }
56
57
  return null;
@@ -77,7 +78,7 @@ export function usageFromGlmQuota(quota, options) {
77
78
  if (!fiveHour || !sevenDay) {
78
79
  return null;
79
80
  }
80
- return makeProviderUsage({
81
+ const usage = makeProviderUsage({
81
82
  provider: "glm",
82
83
  source: options.source,
83
84
  observedAt: options.observedAt,
@@ -85,6 +86,11 @@ export function usageFromGlmQuota(quota, options) {
85
86
  fiveHour,
86
87
  sevenDay,
87
88
  });
89
+ return {
90
+ ...usage,
91
+ quotaUnit: picked.unit,
92
+ pricing: glmPricing(new Date(usage.observedAt)),
93
+ };
88
94
  }
89
95
  export async function collectGlmUsage(config) {
90
96
  if (!config.apiKey) {
package/dist/menubar.js CHANGED
@@ -4,6 +4,7 @@ import { execFileSync } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { ensureDir, isFile } from "./fs-util.js";
6
6
  import { formatProviderLabel, formatResetTime } from "./format.js";
7
+ import { pricingLabel } from "./pricing.js";
7
8
  import { buildPaths } from "./paths.js";
8
9
  import { stableNodeExecutable } from "./node-runtime.js";
9
10
  import { loadDisplayStatusSnapshot } from "./runtime.js";
@@ -768,7 +769,9 @@ function cardProviderBlock(item, now) {
768
769
  darkColor,
769
770
  headline: `${currency}${item.usage.balance.total}`,
770
771
  rows: [],
771
- note: "Pay-as-you-go · no windowed quota",
772
+ note: item.usage.pricing
773
+ ? `Pay-as-you-go · ${pricingLabel(item.usage.pricing, formatResetTime(item.usage.pricing.until, now))}`
774
+ : "Pay-as-you-go · no windowed quota",
772
775
  message: null,
773
776
  updated: observedUpdatedLabel(item, now),
774
777
  };
@@ -792,7 +795,9 @@ function cardProviderBlock(item, now) {
792
795
  darkColor,
793
796
  headline: headlinePct ? `${Math.round(headlinePct.usedPercent)}%` : STATE_LABEL[item.analysis.state],
794
797
  rows,
795
- note: null,
798
+ note: item.usage.pricing
799
+ ? `${pricingLabel(item.usage.pricing, formatResetTime(item.usage.pricing.until, now))}${item.usage.quotaUnit === "credits" ? " · credits" : ""}`
800
+ : null,
796
801
  message: shortCardMessage(item),
797
802
  updated: observedUpdatedLabel(item, now),
798
803
  };
@@ -901,6 +906,9 @@ export function renderMenuBar(snapshot = loadDisplayStatusSnapshot(), paths = bu
901
906
  font: ROW_FONT,
902
907
  size: 12,
903
908
  }));
909
+ if (item.usage.pricing) {
910
+ lines.push(muted(pricingLabel(item.usage.pricing, formatResetTime(item.usage.pricing.until, now))));
911
+ }
904
912
  lines.push(line(item.analysis.message, { color: MUTED_COLOR, size: 12, length: 84 }));
905
913
  lines.push("---");
906
914
  continue;
@@ -920,6 +928,10 @@ export function renderMenuBar(snapshot = loadDisplayStatusSnapshot(), paths = bu
920
928
  if (item.usage.blocked) {
921
929
  lines.push(muted(`Blocked ${item.usage.blocked.reason}. 5h/7d numbers above still reflect the window quotas.`));
922
930
  }
931
+ if (item.usage.pricing) {
932
+ const unit = item.usage.quotaUnit === "credits" ? " · metered in credits" : "";
933
+ lines.push(muted(`${pricingLabel(item.usage.pricing, formatResetTime(item.usage.pricing.until, now))}${unit}`));
934
+ }
923
935
  lines.push(muted(targetLabel(item)));
924
936
  lines.push(line(item.analysis.message, { color: MUTED_COLOR, size: 12, length: 84 }));
925
937
  lines.push("---");
@@ -0,0 +1,5 @@
1
+ import { PricingInfo } from "./types.js";
2
+ export declare function isCnPublicHoliday(date: Date): boolean;
3
+ export declare function deepseekPricing(now?: Date): PricingInfo;
4
+ export declare function glmPricing(now?: Date): PricingInfo;
5
+ export declare function pricingLabel(pricing: PricingInfo, until: string): string;
@@ -0,0 +1,115 @@
1
+ // Peak / off-peak pricing tiers for the two providers that publish a clock-based
2
+ // discount. Both rules are stated by the vendor in Beijing time (UTC+8, no DST),
3
+ // so the arithmetic below deliberately avoids the host time zone.
4
+ //
5
+ // DeepSeek (api-docs.deepseek.com/quick_start/pricing): peak is 01:00-04:00 and
6
+ // 06:00-10:00 UTC, Monday through Friday, excluding Chinese public holidays;
7
+ // every other hour is off-peak at 50% of the peak rate. In Beijing time that is
8
+ // 09:00-12:00 and 14:00-18:00.
9
+ //
10
+ // GLM Coding Plan V3 (docs.bigmodel.cn/cn/coding-plan/overview): 高峰时段 is
11
+ // 每周一至周五的 14:00~18:00 (UTC+8) at the base credit deduction; all other
12
+ // hours deduct 50% of the base credits. The page does not exclude public
13
+ // holidays, and time-boxed promotions (e.g. all-day off-peak during a holiday
14
+ // campaign) are not modelled here: the tier follows the standing rule only.
15
+ const BEIJING_OFFSET_MINUTES = 8 * 60;
16
+ const MINUTES_PER_DAY = 24 * 60;
17
+ const LOOKAHEAD_DAYS = 40;
18
+ // Rest days from 国务院办公厅关于2026年部分节假日安排的通知 (国办发明电〔2025〕7号,
19
+ // 2025-11-04). Adjusted working weekends (调休上班) are intentionally not
20
+ // promoted to weekdays: both vendors phrase the peak rule as Monday-Friday.
21
+ const CN_PUBLIC_HOLIDAYS = {
22
+ 2026: new Set([
23
+ "01-01", "01-02", "01-03",
24
+ "02-15", "02-16", "02-17", "02-18", "02-19", "02-20", "02-21", "02-22", "02-23",
25
+ "04-04", "04-05", "04-06",
26
+ "05-01", "05-02", "05-03", "05-04", "05-05",
27
+ "06-19", "06-20", "06-21",
28
+ "09-25", "09-26", "09-27",
29
+ "10-01", "10-02", "10-03", "10-04", "10-05", "10-06", "10-07",
30
+ ]),
31
+ };
32
+ const DEEPSEEK_RULE = {
33
+ peakSegments: [[9 * 60, 12 * 60], [14 * 60, 18 * 60]],
34
+ excludePublicHolidays: true,
35
+ offPeakDiscountPercent: 50,
36
+ rule: "DeepSeek API: peak Mon-Fri 09:00-12:00 & 14:00-18:00 Asia/Shanghai excluding CN public holidays; off-peak 50% off",
37
+ };
38
+ const GLM_RULE = {
39
+ peakSegments: [[14 * 60, 18 * 60]],
40
+ excludePublicHolidays: false,
41
+ offPeakDiscountPercent: 50,
42
+ rule: "GLM Coding Plan: peak Mon-Fri 14:00-18:00 Asia/Shanghai; off-peak credits deducted at 50%",
43
+ };
44
+ function beijingParts(date) {
45
+ const shifted = new Date(date.getTime() + BEIJING_OFFSET_MINUTES * 60_000);
46
+ return {
47
+ year: shifted.getUTCFullYear(),
48
+ month: shifted.getUTCMonth() + 1,
49
+ day: shifted.getUTCDate(),
50
+ weekday: shifted.getUTCDay(),
51
+ minutes: shifted.getUTCHours() * 60 + shifted.getUTCMinutes(),
52
+ };
53
+ }
54
+ function beijingInstant(year, month, day, minutes) {
55
+ return new Date(Date.UTC(year, month - 1, day, 0, minutes - BEIJING_OFFSET_MINUTES));
56
+ }
57
+ export function isCnPublicHoliday(date) {
58
+ const parts = beijingParts(date);
59
+ const key = `${String(parts.month).padStart(2, "0")}-${String(parts.day).padStart(2, "0")}`;
60
+ return CN_PUBLIC_HOLIDAYS[parts.year]?.has(key) ?? false;
61
+ }
62
+ function tierAt(rule, date) {
63
+ const parts = beijingParts(date);
64
+ if (parts.weekday === 0 || parts.weekday === 6) {
65
+ return "off_peak";
66
+ }
67
+ if (rule.excludePublicHolidays && isCnPublicHoliday(date)) {
68
+ return "off_peak";
69
+ }
70
+ const inPeak = rule.peakSegments.some(([start, end]) => parts.minutes >= start && parts.minutes < end);
71
+ return inPeak ? "peak" : "off_peak";
72
+ }
73
+ // The tier can only flip at a segment edge or at midnight, so walking those
74
+ // instants day by day finds the next change without minute-level scanning.
75
+ function nextTierChange(rule, date) {
76
+ const current = tierAt(rule, date);
77
+ const start = beijingParts(date);
78
+ const edges = Array.from(new Set([0, ...rule.peakSegments.flatMap(([a, b]) => [a, b])]))
79
+ .filter((m) => m < MINUTES_PER_DAY)
80
+ .sort((a, b) => a - b);
81
+ for (let offset = 0; offset <= LOOKAHEAD_DAYS; offset += 1) {
82
+ for (const edge of edges) {
83
+ const candidate = beijingInstant(start.year, start.month, start.day + offset, edge);
84
+ if (candidate.getTime() <= date.getTime()) {
85
+ continue;
86
+ }
87
+ if (tierAt(rule, candidate) !== current) {
88
+ return candidate;
89
+ }
90
+ }
91
+ }
92
+ // Unreachable with the rules above (every week has weekdays); keep a sane bound.
93
+ return beijingInstant(start.year, start.month, start.day + LOOKAHEAD_DAYS, 0);
94
+ }
95
+ function pricingFor(rule, date) {
96
+ const tier = tierAt(rule, date);
97
+ return {
98
+ tier,
99
+ discountPercent: tier === "off_peak" ? rule.offPeakDiscountPercent : 0,
100
+ until: nextTierChange(rule, date).toISOString(),
101
+ timezone: "Asia/Shanghai",
102
+ rule: rule.rule,
103
+ };
104
+ }
105
+ export function deepseekPricing(now = new Date()) {
106
+ return pricingFor(DEEPSEEK_RULE, now);
107
+ }
108
+ export function glmPricing(now = new Date()) {
109
+ return pricingFor(GLM_RULE, now);
110
+ }
111
+ export function pricingLabel(pricing, until) {
112
+ return pricing.tier === "off_peak"
113
+ ? `Off-peak · ${pricing.discountPercent}% off until ${until}`
114
+ : `Peak · full price until ${until}`;
115
+ }
package/dist/types.d.ts CHANGED
@@ -8,11 +8,20 @@ export interface UsageWindow {
8
8
  usedPercent: number;
9
9
  resetsAt: string;
10
10
  }
11
+ export interface PricingInfo {
12
+ tier: "peak" | "off_peak";
13
+ discountPercent: number;
14
+ until: string;
15
+ timezone: string;
16
+ rule: string;
17
+ }
11
18
  export interface ProviderUsage {
12
19
  provider: ProviderId;
13
20
  source: string;
14
21
  observedAt: string;
15
22
  planType?: string | null;
23
+ quotaUnit?: "tokens" | "credits";
24
+ pricing?: PricingInfo;
16
25
  windows: UsageWindow[];
17
26
  balance?: {
18
27
  total: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-usage-bar",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "macOS menu bar usage monitor for Claude Code, Codex, GLM, DeepSeek, and MiniMax.",
5
5
  "author": "hanzhangzzz",
6
6
  "license": "MIT",