@fyeeme/pi-statusline 1.1.1 → 1.2.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/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.0] - 2026-08-21
11
+
12
+ ### Added
13
+
14
+ - Adopt DeepSeek's peak/off-peak pricing (effective 2026-08-17): the price table is split into peak/off-peak columns (flash: cache-hit input ¥0.05/0.10, cache-miss input ¥1.5/3.0, output ¥4.5/9.0; pro: cache-hit input ¥0.15/0.30, cache-miss input ¥4.5/9.0, output ¥13.5/27.0 per million tokens); `isDeepSeekPeakTime()` determines peak hours in Beijing time (UTC+8, no DST; 9:00-12:00, 14:00-18:00), independent of the host timezone.
15
+ - Session cost is recomputed per message using the peak/off-peak rate of the message's own timestamp (`AssistantMessage.timestamp`), so long sessions spanning 9:00/12:00/14:00/18:00 boundaries price each message correctly; the DeepSeek usage segment appends the current period (`peak`/`off-peak`).
16
+ - The registry pricing patch follows the current period (peak or off-peak); the footer render detects period flips and re-aligns the registry so pi's recorded `usage.cost` tracks the period.
17
+
18
+ ### Changed
19
+
20
+ - Pricing logic refactored into a strategy pattern (new `pricing/` module): the `PricingStrategy` interface (`messageCost`/`defaultCurrency`/`applyPricingPatch`/`shouldRefreshPatch`/`footerTag`) defines each provider's price model; DeepSeek is implemented as `DeepSeekPricingStrategy`, dispatched by provider via `getPricingStrategy()`; `cost.ts` removed. Session stats and rendering depend only on the interface — future DeepSeek price changes or GLM/ZAI pricing models only touch/add the corresponding strategy, never the `token-usage`/`index` core flow.
21
+
10
22
  ## [1.1.1] - 2026-08-08
11
23
 
12
24
  ### Fixed
package/README.md CHANGED
@@ -28,6 +28,7 @@ Shows live **account balance** and **weekly token usage**.
28
28
  | Weekly tokens | `7d:1.2M` | Local session file scan (rolling 7 days) |
29
29
 
30
30
  - Currency auto-detected as `¥` (CNY) from the balance API response
31
+ - Session cost follows DeepSeek's peak/off-peak pricing (since 2026-08-17): peak hours are 9:00-12:00 and 14:00-18:00 Beijing time, off-peak is half price. Each message is priced at the rate of its own timestamp; the footer marks the current period (`peak` / `off-peak`).
31
32
  - Balance is fetched on startup and refreshed in the background; cached for 5 minutes
32
33
  - Triggered when `model.provider === "deepseek"`
33
34
 
package/index.ts CHANGED
@@ -22,11 +22,7 @@ import { DeepSeekUsageProvider } from "./providers/deepseek.ts";
22
22
  import { ZaiUsageProvider } from "./providers/zai.ts";
23
23
  import type { UsageProvider } from "./providers/types.ts";
24
24
  import { SessionTokenUsageCalculator } from "./token-usage.ts";
25
- import {
26
- applyDeepSeekPricingPatch,
27
- setCostCurrencyOverride,
28
- getCostCurrencyOverride,
29
- } from "./cost.ts";
25
+ import { deepSeekPricing } from "./pricing/deepseek.ts";
30
26
  import { refreshUsage, getCachedUsage, getUsageCacheAge, resetUsageCache } from "./cache.ts";
31
27
  import { formatCwd, buildStatLine, buildInfoLine } from "./footer.ts";
32
28
 
@@ -51,7 +47,7 @@ let agentRunning = false;
51
47
  let lastElapsedSec = 0;
52
48
  let lastTps = 0;
53
49
 
54
- const tokenCalculator = new SessionTokenUsageCalculator(() => getCostCurrencyOverride());
50
+ const tokenCalculator = new SessionTokenUsageCalculator(() => deepSeekPricing.getCurrencyOverride());
55
51
 
56
52
  // ---------------------------------------------------------------------------
57
53
  // Helpers
@@ -157,20 +153,20 @@ export default function (pi: ExtensionAPI) {
157
153
  handler: async (args, ctx) => {
158
154
  const a = args.trim().toLowerCase();
159
155
  if (a === "auto" || a === "") {
160
- setCostCurrencyOverride(undefined);
156
+ deepSeekPricing.setCurrencyOverride(undefined);
161
157
  ctx.ui.notify("Currency: auto (deepseek→¥ or balance currency)", "info");
162
158
  } else if (a === "¥" || a === "rmb" || a === "cny") {
163
- setCostCurrencyOverride("¥");
159
+ deepSeekPricing.setCurrencyOverride("¥");
164
160
  ctx.ui.notify("Currency: ¥", "info");
165
161
  } else if (a === "$" || a === "usd") {
166
- setCostCurrencyOverride("$");
162
+ deepSeekPricing.setCurrencyOverride("$");
167
163
  ctx.ui.notify("Currency: $", "info");
168
164
  } else {
169
165
  ctx.ui.notify("Usage: /currency [auto|¥|$]", "warning");
170
166
  return;
171
167
  }
172
- // 币种变更后立即对齐注册表定价(CNY→覆盖为官方价;USD→还原 pi 内置价)
173
- applyDeepSeekPricingPatch(ctx.modelRegistry);
168
+ // 币种变更后立即对齐注册表定价(CNY→覆盖为当前时段官方价;USD→还原 pi 内置价)
169
+ deepSeekPricing.applyPricingPatch(ctx.modelRegistry);
174
170
  },
175
171
  });
176
172
 
@@ -178,7 +174,7 @@ export default function (pi: ExtensionAPI) {
178
174
  pi.on("model_select", (event) => {
179
175
  if (lastCtx) {
180
176
  lastModel = event.model;
181
- applyDeepSeekPricingPatch(lastCtx.modelRegistry);
177
+ deepSeekPricing.applyPricingPatch(lastCtx.modelRegistry);
182
178
  void refreshUsage(providers, lastCtx.modelRegistry, event.model);
183
179
  }
184
180
  });
@@ -198,7 +194,7 @@ export default function (pi: ExtensionAPI) {
198
194
  lastTps = 0;
199
195
 
200
196
  // 根治:运行时修正 deepseek 定价(pi 内置定价低估约 7 倍),使后续 usage.cost 记录全局正确
201
- applyDeepSeekPricingPatch(ctx.modelRegistry);
197
+ deepSeekPricing.applyPricingPatch(ctx.modelRegistry);
202
198
 
203
199
  // Prime usage cache on startup (non-blocking)
204
200
  lastCtx = ctx;
@@ -212,6 +208,11 @@ export default function (pi: ExtensionAPI) {
212
208
  dispose: unsub,
213
209
  invalidate() {},
214
210
  render(width: number): string[] {
211
+ // 峰谷时段翻转(如跨 9:00/12:00/14:00/18:00 边界)后重新对齐注册表定价,使 pi 后续
212
+ // 记录的 usage.cost 跟随时段;幂等,仅在翻转时 registerProvider 一次。
213
+ if (deepSeekPricing.shouldRefreshPatch()) {
214
+ deepSeekPricing.applyPricingPatch(ctx.modelRegistry);
215
+ }
215
216
  const stats = tokenCalculator.compute(ctx);
216
217
  const cu = ctx.getContextUsage();
217
218
  const model = ctx.model;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fyeeme/pi-statusline",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Rich custom status bar for pi with token usage, cost, DeepSeek balance, context window, and timing.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,6 +33,7 @@
33
33
  "files": [
34
34
  "*.ts",
35
35
  "providers/*.ts",
36
+ "pricing/*.ts",
36
37
  "README.md",
37
38
  "LICENSE",
38
39
  "CHANGELOG.md"
@@ -0,0 +1,162 @@
1
+ /**
2
+ * DeepSeek 定价策略(跟随结算币种 + 峰谷时段)。
3
+ *
4
+ * 背景:pi 内置的 deepseek 定价数据(pi-ai 的 providers/data/deepseek.json)用的是旧版官方 USD 价
5
+ * (flash $0.14/0.28、pro $0.435/0.87 每百万 tokens)。DeepSeek 账户分 CNY 与 USD 两种结算:
6
+ * - CNY 账户按官方 ¥ 价计费,且自 2026-08-17 起实施峰谷计价:高峰时段(北京时间 9:00-12:00、
7
+ * 14:00-18:00)为官价,空闲时段为高峰的一半(见官方定价页备注);
8
+ * - USD 账户按 USD 计费,pi 内置价负责(本策略不覆盖)。
9
+ *
10
+ * 因此所有「修正」都必须跟随结算币种与峰谷时段:
11
+ * 1. `billedInCny()`:判定是否按 CNY 计价 —— 用户显式 `/currency` 优先;否则按 balance API
12
+ * 探测到的账户币种(探测失败/未知时按 CNY,保持原有「deepseek → ¥」默认行为)。
13
+ * 2. `isDeepSeekPeakTime()`:按北京时间(UTC+8,无夏令时)判定指定时刻是否高峰时段。
14
+ * 3. `messageCost()`:按消息时刻对应的峰谷单价重算单条消息成本(仅 CNY 计价时接管;USD 计价
15
+ * 返回 null 回退 pi 记录的 cost —— 会话文件里已记录的 cost 无法追溯修正)。
16
+ * 4. `applyPricingPatch()`:运行时把注册表中 deepseek 模型定价覆盖为当前时段的官方 CNY 价
17
+ * (仅 CNY 计价时;USD 计价时还原为 pi 内置 USD 价)。内存补丁,不修改数据文件、pi 升级不丢。
18
+ */
19
+ import type { Model, Api } from "@earendil-works/pi-ai";
20
+ import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
21
+ import type { PricedUsage, PricingStrategy } from "./types.ts";
22
+
23
+ export interface DeepSeekPricedUsage extends PricedUsage {}
24
+
25
+ export interface DeepSeekCnyPrice {
26
+ /** 高峰时段单价(元 / 百万 tokens) */
27
+ peak: DeepSeekPricedUsage;
28
+ /** 空闲时段单价(元 / 百万 tokens)= 高峰的一半(官方规则) */
29
+ offPeak: DeepSeekPricedUsage;
30
+ }
31
+
32
+ /**
33
+ * DeepSeek 官方 CNY 单价(元 / 百万 tokens),2026-08-17 峰谷计价起生效(见官方定价页):
34
+ * 缓存命中输入 / 未命中输入 / 输出;cacheWrite 官方未单列(按 0 计)。
35
+ */
36
+ export const DEEPSEEK_CNY_PRICES: Record<string, DeepSeekCnyPrice> = {
37
+ "deepseek-v4-flash": {
38
+ peak: { input: 3.0, output: 9.0, cacheRead: 0.1, cacheWrite: 0 },
39
+ offPeak: { input: 1.5, output: 4.5, cacheRead: 0.05, cacheWrite: 0 },
40
+ },
41
+ "deepseek-v4-pro": {
42
+ peak: { input: 9.0, output: 27.0, cacheRead: 0.3, cacheWrite: 0 },
43
+ offPeak: { input: 4.5, output: 13.5, cacheRead: 0.15, cacheWrite: 0 },
44
+ },
45
+ };
46
+
47
+ const PER_MILLION = 1_000_000;
48
+
49
+ /**
50
+ * DeepSeek 高峰时段判定:北京时间(UTC+8,无夏令时)9:00-12:00、14:00-18:00 为高峰,
51
+ * 其余为空闲时段(官方定价页备注)。`at` 可指定时刻,默认当前时间。
52
+ */
53
+ export function isDeepSeekPeakTime(at: Date = new Date()): boolean {
54
+ const beijingHour = (at.getUTCHours() + 8) % 24;
55
+ return (beijingHour >= 9 && beijingHour < 12) || (beijingHour >= 14 && beijingHour < 18);
56
+ }
57
+
58
+ export class DeepSeekPricingStrategy implements PricingStrategy {
59
+ readonly providerId = "deepseek";
60
+
61
+ /** balance API 探测到的账户结算币种(CNY / USD / 其他)。 */
62
+ private detectedCurrency: string | null = null;
63
+ /** 用户显式 `/currency` 设置,覆盖自动判定。 */
64
+ private currencyOverride: "¥" | "$" | undefined = undefined;
65
+ /** 首次 applyPricingPatch 时缓存的原始模型定义(用于 USD 计价时还原)。 */
66
+ private originalModels: Model<Api>[] | null = null;
67
+ /** 上次注册表补丁写入时的时段(用于峰谷翻转后廉价重新对齐)。 */
68
+ private lastPatchedPeriod: "peak" | "offPeak" | null = null;
69
+
70
+ /** 记录 balance API 探测到的账户结算币种(fetchUsage 成功后写入;null 表示未知)。 */
71
+ setDetectedCurrency(currency: string | null): void {
72
+ this.detectedCurrency = currency;
73
+ }
74
+
75
+ /** 设置用户显式币种偏好(`/currency` 命令);undefined 恢复自动判定。 */
76
+ setCurrencyOverride(override: "¥" | "$" | undefined): void {
77
+ this.currencyOverride = override;
78
+ }
79
+
80
+ /** 当前用户显式币种偏好(undefined = 自动)。 */
81
+ getCurrencyOverride(): "¥" | "$" | undefined {
82
+ return this.currencyOverride;
83
+ }
84
+
85
+ /**
86
+ * 是否按 CNY 计价:用户显式偏好优先;否则仅当探测明确为 USD 时按 USD 计价,
87
+ * 其余情况(CNY 或未知)按 CNY —— 保持原有「deepseek → ¥」的默认行为。
88
+ */
89
+ billedInCny(): boolean {
90
+ if (this.currencyOverride === "$") return false;
91
+ if (this.currencyOverride === "¥") return true;
92
+ return this.detectedCurrency !== "USD";
93
+ }
94
+
95
+ /** 重置策略状态与补丁缓存(测试用)。 */
96
+ reset(): void {
97
+ this.detectedCurrency = null;
98
+ this.currencyOverride = undefined;
99
+ this.originalModels = null;
100
+ this.lastPatchedPeriod = null;
101
+ }
102
+
103
+ messageCost(model: string | undefined, usage: PricedUsage, at: Date): number | null {
104
+ // 仅 CNY 计价时接管重算;USD 结算用户沿用 pi 记录的 cost
105
+ if (!this.billedInCny()) return null;
106
+ if (!model) return null;
107
+ const p = DEEPSEEK_CNY_PRICES[model];
108
+ if (!p) return null;
109
+ const rates = isDeepSeekPeakTime(at) ? p.peak : p.offPeak;
110
+ return (
111
+ (rates.input * usage.input +
112
+ rates.output * usage.output +
113
+ rates.cacheRead * usage.cacheRead +
114
+ rates.cacheWrite * usage.cacheWrite) /
115
+ PER_MILLION
116
+ );
117
+ }
118
+
119
+ defaultCurrency(): "¥" | "$" | null {
120
+ return this.billedInCny() ? "¥" : null;
121
+ }
122
+
123
+ applyPricingPatch(registry: ModelRegistry): void {
124
+ const dsModels = registry.getAll().filter((m) => m.provider === this.providerId);
125
+ if (dsModels.length === 0) return;
126
+ if (!this.originalModels) {
127
+ this.originalModels = dsModels;
128
+ } else {
129
+ // Merge in deepseek models added after the first snapshot, so a later
130
+ // re-patch(时段翻转 / 币种切换 / model_select / 探测后纠正)不会丢掉它们:
131
+ // registerProvider 是整表替换,用陈旧快照 map 会把新增模型从 registry 里静默删掉。
132
+ const known = new Set(this.originalModels.map((m) => m.id));
133
+ for (const m of dsModels) if (!known.has(m.id)) this.originalModels.push(m);
134
+ }
135
+
136
+ const billedInCny = this.billedInCny();
137
+ const peak = isDeepSeekPeakTime();
138
+ this.lastPatchedPeriod = peak ? "peak" : "offPeak";
139
+ const models = this.originalModels.map((m) => {
140
+ if (!billedInCny) return m;
141
+ const cny = DEEPSEEK_CNY_PRICES[m.id];
142
+ if (!cny) return m;
143
+ const rates = peak ? cny.peak : cny.offPeak;
144
+ return { ...m, cost: { ...m.cost, ...rates } };
145
+ });
146
+ registry.registerProvider("deepseek", { models });
147
+ }
148
+
149
+ shouldRefreshPatch(): boolean {
150
+ if (this.lastPatchedPeriod === null) return false;
151
+ return isDeepSeekPeakTime() !== (this.lastPatchedPeriod === "peak");
152
+ }
153
+
154
+ footerTag(): string | null {
155
+ // 峰谷计价(2026-08-17 起)生效后,标注当前时段:CNY 计价按官方峰谷价重算,直接可见;
156
+ // USD 计价走 pi 内置价(无峰谷概念),不标注避免误导。
157
+ return this.billedInCny() ? (isDeepSeekPeakTime() ? "peak" : "off-peak") : null;
158
+ }
159
+ }
160
+
161
+ /** DeepSeek 定价策略单例(注册表与消费方共用同一份状态)。 */
162
+ export const deepSeekPricing = new DeepSeekPricingStrategy();
@@ -0,0 +1,16 @@
1
+ import type { PricingStrategy } from "./types.ts";
2
+ import { deepSeekPricing } from "./deepseek.ts";
3
+
4
+ /**
5
+ * 已注册的定价策略(按 provider id 索引)。新 provider 引入自有价格体系(如未来 GLM/ZAI
6
+ * 跟进峰谷计价)时,实现 `PricingStrategy` 并在此注册,会话统计与渲染无需改动。
7
+ */
8
+ export const pricingStrategies: Record<string, PricingStrategy> = {
9
+ [deepSeekPricing.providerId]: deepSeekPricing,
10
+ };
11
+
12
+ /** 按 provider id 取定价策略(大小写不敏感);未注册返回 undefined(成本走 pi 记录值、币种走 `$`)。 */
13
+ export function getPricingStrategy(provider: string | undefined): PricingStrategy | undefined {
14
+ if (!provider) return undefined;
15
+ return pricingStrategies[provider.toLowerCase()];
16
+ }
@@ -0,0 +1,39 @@
1
+ import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
2
+
3
+ /** 消息 token 用量分解(会话统计所需的子集)。 */
4
+ export interface PricedUsage {
5
+ input: number;
6
+ output: number;
7
+ cacheRead: number;
8
+ cacheWrite: number;
9
+ }
10
+
11
+ /**
12
+ * Provider 定价策略:把「该 provider 当前生效的价格体系」收敛到一个实现里。
13
+ * 价格调整(如 DeepSeek 2026-08 峰谷计价、未来 GLM/ZAI 调价)只改对应策略,
14
+ * 会话统计(token-usage)与渲染(index/footer)只依赖本接口,不感知具体 provider。
15
+ */
16
+ export interface PricingStrategy {
17
+ /** 适用 provider id(小写,如 `deepseek`)。 */
18
+ readonly providerId: string;
19
+
20
+ /**
21
+ * 按消息时刻重算单条消息成本;返回 null 表示不接管(会话统计回退 pi 记录的
22
+ * `usage.cost`,如 USD 结算的 DeepSeek 或未实现定价策略的 provider)。
23
+ *
24
+ * @param at 消息产生时刻;按时段计价的策略(如峰谷)据此选单价
25
+ */
26
+ messageCost(model: string | undefined, usage: PricedUsage, at: Date): number | null;
27
+
28
+ /** 该 provider 的成本显示币种偏好;null 表示不接管(通用逻辑按 `$`)。 */
29
+ defaultCurrency(): "¥" | "$" | null;
30
+
31
+ /** 把注册表中该 provider 模型的定价对齐当前价格体系;幂等可逆,无覆盖需求时 no-op。 */
32
+ applyPricingPatch(registry: ModelRegistry): void;
33
+
34
+ /** 注册表定价是否需要按当前状态重新对齐(如峰谷时段翻转);无则 false。 */
35
+ shouldRefreshPatch(): boolean;
36
+
37
+ /** footer 附加标注(如当前峰谷时段 `峰`/`闲`);无则 null。 */
38
+ footerTag(): string | null;
39
+ }
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { DeepSeekResult, ProviderUsageResult } from "../types.ts";
3
3
  import type { UsageProvider } from "./types.ts";
4
- import { applyDeepSeekPricingPatch, setDetectedDeepSeekCurrency } from "../cost.ts";
4
+ import { deepSeekPricing } from "../pricing/deepseek.ts";
5
5
  import { scanWeeklyTokens } from "../session-scanner.ts";
6
6
  import { fmt } from "../footer.ts";
7
7
 
@@ -41,11 +41,11 @@ export class DeepSeekUsageProvider implements UsageProvider {
41
41
  if (!info) return null;
42
42
 
43
43
  // 记录账户结算币种:CNY 账户按官方 ¥ 价计费;USD 账户保持 pi 内置 USD 价(不覆盖)
44
- setDetectedDeepSeekCurrency(info.currency ?? null);
45
- // 探测到币种后立即重新对齐注册表定价:session_start 时(detectedDeepSeekCurrency 尚为
46
- // null)已按默认 CNY 覆盖过一次;此处按真实币种纠正,避免 USD 账户停在被高估约 7 倍的
47
- // CNY 价上,直到下一次 model_select/​/currency 才修正。
48
- applyDeepSeekPricingPatch(modelRegistry);
44
+ deepSeekPricing.setDetectedCurrency(info.currency ?? null);
45
+ // 探测到币种后立即重新对齐注册表定价:session_start 时(币种尚为 null)已按默认 CNY
46
+ // 覆盖过一次;此处按真实币种纠正,避免 USD 账户停在被高估约 7 倍的 CNY 价上,直到
47
+ // 下一次 model_select/​/currency 才修正。
48
+ deepSeekPricing.applyPricingPatch(modelRegistry);
49
49
 
50
50
  const weeklyTokens = scanWeeklyTokens("deepseek");
51
51
 
@@ -73,6 +73,9 @@ export class DeepSeekUsageProvider implements UsageProvider {
73
73
  parts.push(balance);
74
74
  }
75
75
  if (weekly) parts.push(weekly);
76
+ // 峰谷时段标注(如当前为高峰 → "峰");USD 计价不标注(见策略 footerTag)
77
+ const tag = deepSeekPricing.footerTag();
78
+ if (tag) parts.push(tag);
76
79
 
77
80
  return parts.join(" · ");
78
81
  }
package/token-usage.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { AssistantMessage } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import type { TokenUsage, TokenUsageCalculator } from "./types.ts";
4
- import { deepSeekBilledInCny, deepseekMessageCostCny } from "./cost.ts";
4
+ import { getPricingStrategy } from "./pricing/index.ts";
5
5
 
6
6
  export class SessionTokenUsageCalculator implements TokenUsageCalculator {
7
7
  private getCurrencyOverride: () => "\u00a5" | "$" | undefined;
@@ -18,7 +18,8 @@ export class SessionTokenUsageCalculator implements TokenUsageCalculator {
18
18
  let total = 0;
19
19
  let cost = 0;
20
20
 
21
- const billedInCny = deepSeekBilledInCny();
21
+ // 定价策略按当前 provider 接管成本重算;未注册(或策略不接管)时回退 pi 记录的 cost。
22
+ const strategy = getPricingStrategy(ctx.model?.provider);
22
23
 
23
24
  // Use getBranch() (current branch only) so token/cost stats exclude abandoned
24
25
  // sibling branches after /fork or /tree navigation. getEntries() would sum
@@ -31,9 +32,12 @@ export class SessionTokenUsageCalculator implements TokenUsageCalculator {
31
32
  cacheRead += m.usage.cacheRead;
32
33
  cacheWrite += m.usage.cacheWrite;
33
34
  total += m.usage.totalTokens;
34
- // DeepSeek 仅按 CNY 计价时重算(pi 内置 USD 价低估约 7 倍);USD 结算用户沿用 pi 记录的 cost
35
- const dsCost = billedInCny ? deepseekMessageCostCny(m.model, m.usage) : null;
36
- cost += dsCost !== null ? dsCost : (m.usage.cost?.total ?? 0);
35
+ // 策略接管时按消息时刻重算(如 DeepSeek CNY 峰谷价:跨 9:00/12:00/14:00/18:00
36
+ // 边界的长会话各条消息各算各的);否则沿用 pi 记录的 cost。
37
+ const priced = strategy
38
+ ? strategy.messageCost(m.model, m.usage, m.timestamp ? new Date(m.timestamp) : new Date())
39
+ : null;
40
+ cost += priced !== null ? priced : (m.usage.cost?.total ?? 0);
37
41
  }
38
42
  }
39
43
 
@@ -44,8 +48,7 @@ export class SessionTokenUsageCalculator implements TokenUsageCalculator {
44
48
  if (override) {
45
49
  currency = override;
46
50
  } else {
47
- const p = ctx.model?.provider ?? "";
48
- if (p.toLowerCase().includes("deepseek")) currency = billedInCny ? "\u00a5" : "$";
51
+ currency = strategy?.defaultCurrency() ?? "$";
49
52
  }
50
53
 
51
54
  return { input, output, cacheRead, cacheWrite, total, hitRate, cost, currency };
package/cost.ts DELETED
@@ -1,135 +0,0 @@
1
- /**
2
- * DeepSeek 定价修正(跟随结算币种)。
3
- *
4
- * 背景:pi 内置的 deepseek 定价数据(pi-ai 的 providers/data/deepseek.json)用的是官方 USD 价
5
- * (flash $0.14/0.28/0.0028、pro $0.435/0.87/0.003625 每百万 tokens)。DeepSeek 账户分 CNY 与 USD
6
- * 两种结算:CNY 账户按 ¥1/¥2/¥0.02、¥3/¥6/¥0.025 计费,此时 pi 记录的成本低估约 7 倍;USD 账户
7
- * 按 USD 计费,pi 内置价本来就是对的。
8
- *
9
- * 因此所有"修正"都必须跟随结算币种:
10
- * 1. `deepSeekBilledInCny()`:判定 DeepSeek 是否按 CNY 计价 —— 用户显式 `/currency` 优先;
11
- * 否则按 balance API 探测到的账户币种(探测失败/未知时按 CNY,保持原行为)。
12
- * 2. `deepseekMessageCostCny`:按官方 CNY 单价重算单条消息成本(仅 CNY 计价时用于兜底存量消息,
13
- * 会话文件里已记录的 cost 无法追溯修正)。
14
- * 3. `applyDeepSeekPricingPatch`:运行时把注册表中 deepseek 模型定价覆盖为官方 CNY 价(仅 CNY
15
- * 计价时;USD 计价时还原为 pi 内置 USD 价)。内存补丁,不修改数据文件、pi 升级不丢。
16
- */
17
- import type { Model, Api } from "@earendil-works/pi-ai";
18
- import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
19
-
20
- export interface DeepSeekPricedUsage {
21
- input: number;
22
- output: number;
23
- cacheRead: number;
24
- cacheWrite: number;
25
- }
26
-
27
- /** DeepSeek 官方 CNY 单价(元 / 百万 tokens):缓存命中输入 / 未命中输入 / 输出(见官方定价页)。 */
28
- export const DEEPSEEK_CNY_PRICES: Record<string, DeepSeekPricedUsage> = {
29
- "deepseek-v4-flash": { input: 1, output: 2, cacheRead: 0.02, cacheWrite: 0 },
30
- "deepseek-v4-pro": { input: 3, output: 6, cacheRead: 0.025, cacheWrite: 0 },
31
- };
32
-
33
- const PER_MILLION = 1_000_000;
34
-
35
- // ---------------------------------------------------------------------------
36
- // 结算币种状态(DeepSeek)
37
- // ---------------------------------------------------------------------------
38
-
39
- /** balance API 探测到的账户结算币种(CNY / USD / 其他)。 */
40
- let detectedDeepSeekCurrency: string | null = null;
41
- /** 用户显式 `/currency` 设置,覆盖自动判定。 */
42
- let costCurrencyOverride: "¥" | "$" | undefined = undefined;
43
- /** 首次 applyDeepSeekPricingPatch 时缓存的原始模型定义(用于 USD 计价时还原)。 */
44
- let originalDeepSeekModels: Model<Api>[] | null = null;
45
-
46
- /** 记录 balance API 探测到的 DeepSeek 账户结算币种(fetchUsage 成功后写入;null 表示未知)。 */
47
- export function setDetectedDeepSeekCurrency(currency: string | null): void {
48
- detectedDeepSeekCurrency = currency;
49
- }
50
-
51
- /** 设置用户显式币种偏好(`/currency` 命令);undefined 恢复自动判定。 */
52
- export function setCostCurrencyOverride(override: "¥" | "$" | undefined): void {
53
- costCurrencyOverride = override;
54
- }
55
-
56
- /** 当前用户显式币种偏好(undefined = 自动)。 */
57
- export function getCostCurrencyOverride(): "¥" | "$" | undefined {
58
- return costCurrencyOverride;
59
- }
60
-
61
- /**
62
- * DeepSeek 是否按 CNY 计价:用户显式偏好优先;否则仅当探测明确为 USD 时按 USD 计价,
63
- * 其余情况(CNY 或未知)按 CNY —— 保持原有"deepseek → ¥"的默认行为。
64
- */
65
- export function deepSeekBilledInCny(): boolean {
66
- if (costCurrencyOverride === "$") return false;
67
- if (costCurrencyOverride === "¥") return true;
68
- return detectedDeepSeekCurrency !== "USD";
69
- }
70
-
71
- /** 重置币种状态与补丁缓存(测试用)。 */
72
- export function resetDeepSeekPricingState(): void {
73
- detectedDeepSeekCurrency = null;
74
- costCurrencyOverride = undefined;
75
- originalDeepSeekModels = null;
76
- }
77
-
78
- // ---------------------------------------------------------------------------
79
- // 单条消息成本重算
80
- // ---------------------------------------------------------------------------
81
-
82
- /**
83
- * 按官方 CNY 单价重算单条消息成本;模型不在定价表内返回 null(调用方回退 `m.usage.cost`)。
84
- *
85
- * @param model 消息模型 id(如 `deepseek-v4-flash`)
86
- * @param usage 消息的 token 用量分解
87
- */
88
- export function deepseekMessageCostCny(
89
- model: string | undefined,
90
- usage: DeepSeekPricedUsage,
91
- ): number | null {
92
- if (!model) return null;
93
- const p = DEEPSEEK_CNY_PRICES[model];
94
- if (!p) return null;
95
- return (
96
- (p.input * usage.input +
97
- p.output * usage.output +
98
- p.cacheRead * usage.cacheRead +
99
- p.cacheWrite * usage.cacheWrite) /
100
- PER_MILLION
101
- );
102
- }
103
-
104
- // ---------------------------------------------------------------------------
105
- // 运行时注册表补丁(根治 pi 内置定价,可逆、跟随结算币种)
106
- // ---------------------------------------------------------------------------
107
-
108
- /**
109
- * 把注册表中 deepseek 模型的定价与结算币种对齐:
110
- * - CNY 计价:cost 覆盖为官方 CNY 价;
111
- * - USD 计价:还原为 pi 内置 USD 价(首次调用时缓存原始定义,之后不再被 CNY 覆盖)。
112
- * 未知/未来新增模型原样保留;重复调用幂等;注册表未加载模型时静默跳过(由调用方后续重试)。
113
- */
114
- export function applyDeepSeekPricingPatch(registry: ModelRegistry): void {
115
- const dsModels = registry.getAll().filter((m) => m.provider === "deepseek");
116
- if (dsModels.length === 0) return;
117
- if (!originalDeepSeekModels) {
118
- originalDeepSeekModels = dsModels;
119
- } else {
120
- // Merge in deepseek models added after the first snapshot, so a later
121
- // re-patch(币种切换 / model_select / 探测后纠正)不会丢掉它们:registerProvider
122
- // 是整表替换,用陈旧快照 map 会把新增模型从 registry 里静默删掉。
123
- const known = new Set(originalDeepSeekModels.map((m) => m.id));
124
- for (const m of dsModels) if (!known.has(m.id)) originalDeepSeekModels.push(m);
125
- }
126
-
127
- const billedInCny = deepSeekBilledInCny();
128
- const models = originalDeepSeekModels.map((m) => {
129
- if (!billedInCny) return m;
130
- const cny = DEEPSEEK_CNY_PRICES[m.id];
131
- if (!cny) return m;
132
- return { ...m, cost: { ...m.cost, ...cny } };
133
- });
134
- registry.registerProvider("deepseek", { models });
135
- }