@fyeeme/pi-statusline 1.1.0 → 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 +30 -0
- package/README.md +4 -1
- package/cache.ts +7 -0
- package/index.ts +31 -29
- package/package.json +9 -9
- package/pricing/deepseek.ts +162 -0
- package/pricing/index.ts +16 -0
- package/pricing/types.ts +39 -0
- package/providers/deepseek.ts +9 -6
- package/providers/zai.ts +4 -4
- package/token-usage.ts +14 -8
- package/types.ts +1 -1
- package/cost.ts +0 -135
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@ 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
|
+
|
|
22
|
+
## [1.1.1] - 2026-08-08
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- 会话级 token/cost/tok-s 在分叉会话(`/fork`、`/tree`)后不再把整棵树计入:`SessionTokenUsageCalculator.compute` 与 `agent_end` 累计输出改用 `sessionManager.getBranch()`(仅当前分支);`/status-debug` 仍保留全量 `getEntries()` 用于诊断。
|
|
27
|
+
- 切换 provider(`/model` deepseek↔zai)后状态栏不再短暂显示上一个 provider 的用量段:`getCachedUsage` 在缓存 provider 与当前 provider 不匹配时返回 null,等待 `model_select` 触发的新刷新落地。
|
|
28
|
+
- ZAI 用量段对 `zai-coding-cn` provider 不再永久漏显示:`ZaiResult.provider` 记录实际 provider key(`zai`/`zai-coding-cn`),provider guard 不再因字面量 `"zai"` 误判(code-review #1)。
|
|
29
|
+
- tok/s 改用本次 agent run 的 output/耗时(`event.messages`),分子分母同口径,不再受 /fork 放弃分支的累计耗时影响;同时消除 `agent_end` 对模块级 `lastCtx` 的依赖(code-review #2)。
|
|
30
|
+
- `/status-debug` 的 msg 遍历改用 `getBranch()`,与 `tokenCalculator.compute` 口径一致(code-review #7)。
|
|
31
|
+
- `SessionTokenUsageCalculator.compute` 内 `deepSeekBilledInCny()` 冗余的第二次调用复用已有变量(code-review #8)。
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- 移除 `package.json` 中已废弃的 MCP 描述与 keyword(MCP 状态段自 1.0.2 起已移除)。
|
|
36
|
+
- 新增 `session_shutdown` 清理 usage cache:`/resume` 或 `/new` 进入不同 provider 时立即刷新,而非沿用上一会话缓存。
|
|
37
|
+
- `devDependencies` 对齐当前平台(`@earendil-works/pi-coding-agent`/`pi-ai`/`pi-tui` `0.77.0` → `0.84.1`),便于利用 `thinking_level_select`、`getAvailableProviderCount` 等新类型。
|
|
38
|
+
- README 状态栏示例 `deepseek-v3` → `deepseek-v4-pro`(`deepseek-v3` 已不在 0.84.x 模型库)。
|
|
39
|
+
|
|
10
40
|
## [1.1.0] - 2026-08-07
|
|
11
41
|
|
|
12
42
|
### Added
|
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
|
|
|
@@ -95,7 +96,7 @@ See the Pi Packages guide on [pi.dev](https://pi.dev) for the full list of sourc
|
|
|
95
96
|
## Status Bar Layout
|
|
96
97
|
|
|
97
98
|
```
|
|
98
|
-
~/projects/my-repo (main) deepseek-
|
|
99
|
+
~/projects/my-repo (main) deepseek-v4-pro · xhigh
|
|
99
100
|
tokens 65k(in 12k, out 8k, cache 45k) · ¥0.12/50.00 · 7d:1.2M · 45.2%/64k · 2m30s 38.2tok/s
|
|
100
101
|
```
|
|
101
102
|
|
|
@@ -120,6 +121,8 @@ The extension registers a `UsageProvider` per supported provider and refreshes u
|
|
|
120
121
|
|
|
121
122
|
All network calls are best-effort with a 5-second timeout — failures stay silent and never block the status bar.
|
|
122
123
|
|
|
124
|
+
> **Platform API note:** the custom footer relies on `ctx.ui.setFooter`'s third parameter — `footerData: ReadonlyFooterDataProvider` (for `getGitBranch`/`getExtensionStatuses`/`onBranchChange`) — and on returning `dispose` from the footer factory. These are part of the 0.84.x `ExtensionAPI` surface (see `dist/core/extensions/types.d.ts`), but pi's `docs/extensions.md` currently documents only the `(tui, theme)` form. If a future pi release changes the `setFooter` signature, check this extension's footer wiring first.
|
|
125
|
+
|
|
123
126
|
## Changelog
|
|
124
127
|
|
|
125
128
|
See [CHANGELOG.md](CHANGELOG.md).
|
package/cache.ts
CHANGED
|
@@ -70,6 +70,13 @@ export function getCachedUsage(
|
|
|
70
70
|
// may synchronously set usageCache = null when the provider is not found,
|
|
71
71
|
// which would make the eventual `return cache.result` throw on null.
|
|
72
72
|
const cache = usageCache;
|
|
73
|
+
// Drop stale cache if the active provider changed since it was populated
|
|
74
|
+
// (e.g. deepseek -> zai via /model). model_select already kicked off a
|
|
75
|
+
// refresh; returning null avoids briefly showing the previous provider's
|
|
76
|
+
// segment until the new fetch lands.
|
|
77
|
+
if (lastModel && cache.result && cache.result.provider !== lastModel.provider) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
73
80
|
if (Date.now() - cache.fetchedAt > CACHE_TTL_MS) {
|
|
74
81
|
// TTL expired: return stale data but trigger background refresh.
|
|
75
82
|
// Memoize inflight promise so concurrent callers share one fetch.
|
package/index.ts
CHANGED
|
@@ -22,12 +22,8 @@ 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
|
-
|
|
27
|
-
setCostCurrencyOverride,
|
|
28
|
-
getCostCurrencyOverride,
|
|
29
|
-
} from "./cost.ts";
|
|
30
|
-
import { refreshUsage, getCachedUsage, getUsageCacheAge } from "./cache.ts";
|
|
25
|
+
import { deepSeekPricing } from "./pricing/deepseek.ts";
|
|
26
|
+
import { refreshUsage, getCachedUsage, getUsageCacheAge, resetUsageCache } from "./cache.ts";
|
|
31
27
|
import { formatCwd, buildStatLine, buildInfoLine } from "./footer.ts";
|
|
32
28
|
|
|
33
29
|
// ---------------------------------------------------------------------------
|
|
@@ -51,7 +47,7 @@ let agentRunning = false;
|
|
|
51
47
|
let lastElapsedSec = 0;
|
|
52
48
|
let lastTps = 0;
|
|
53
49
|
|
|
54
|
-
const tokenCalculator = new SessionTokenUsageCalculator(() =>
|
|
50
|
+
const tokenCalculator = new SessionTokenUsageCalculator(() => deepSeekPricing.getCurrencyOverride());
|
|
55
51
|
|
|
56
52
|
// ---------------------------------------------------------------------------
|
|
57
53
|
// Helpers
|
|
@@ -79,21 +75,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
79
75
|
if (elapsedMs <= 0) return;
|
|
80
76
|
lastElapsedSec += elapsedMs / 1000;
|
|
81
77
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
91
|
-
// fallback: use current turn's messages when no session context
|
|
92
|
-
for (const m of event.messages) {
|
|
93
|
-
if (m.role === "assistant") cumulativeOutput += (m as AssistantMessage).usage.output;
|
|
94
|
-
}
|
|
78
|
+
// tok/s = 本次 agent run 的 output / 本次 run 耗时(即时速度)。用 event.messages
|
|
79
|
+
// 而非 sessionManager 遍历:不依赖 lastCtx/分支语义,不受 /fork 放弃分支的耗时
|
|
80
|
+
// 累计影响(分子分母同为本 run 口径),也避开 lastCtx 在 session 切换边界的 staleness。
|
|
81
|
+
let turnOutput = 0;
|
|
82
|
+
for (const m of event.messages) {
|
|
83
|
+
if (m.role === "assistant") turnOutput += (m as AssistantMessage).usage.output;
|
|
95
84
|
}
|
|
96
|
-
lastTps =
|
|
85
|
+
lastTps = turnOutput > 0 ? turnOutput / (elapsedMs / 1000) : 0;
|
|
97
86
|
|
|
98
87
|
// Schedule background refresh to pick up new usage data
|
|
99
88
|
if (lastCtx && lastModel) {
|
|
@@ -132,8 +121,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
132
121
|
const cacheAge = getUsageCacheAge();
|
|
133
122
|
w(` cacheAge: ${cacheAge !== null ? `${Math.round(cacheAge / 1000)}s` : "N/A"}`);
|
|
134
123
|
|
|
124
|
+
// status-debug dumps the current branch (same scope tokenCalculator.compute
|
|
125
|
+
// uses via getBranch), so the msg count (idx) lines up with computed totals.
|
|
135
126
|
let idx = 0;
|
|
136
|
-
for (const entry of ctx.sessionManager.
|
|
127
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
137
128
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
138
129
|
const m = entry.message as AssistantMessage;
|
|
139
130
|
w(`--- msg[${idx}] ---`);
|
|
@@ -162,20 +153,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
162
153
|
handler: async (args, ctx) => {
|
|
163
154
|
const a = args.trim().toLowerCase();
|
|
164
155
|
if (a === "auto" || a === "") {
|
|
165
|
-
|
|
156
|
+
deepSeekPricing.setCurrencyOverride(undefined);
|
|
166
157
|
ctx.ui.notify("Currency: auto (deepseek→¥ or balance currency)", "info");
|
|
167
158
|
} else if (a === "¥" || a === "rmb" || a === "cny") {
|
|
168
|
-
|
|
159
|
+
deepSeekPricing.setCurrencyOverride("¥");
|
|
169
160
|
ctx.ui.notify("Currency: ¥", "info");
|
|
170
161
|
} else if (a === "$" || a === "usd") {
|
|
171
|
-
|
|
162
|
+
deepSeekPricing.setCurrencyOverride("$");
|
|
172
163
|
ctx.ui.notify("Currency: $", "info");
|
|
173
164
|
} else {
|
|
174
165
|
ctx.ui.notify("Usage: /currency [auto|¥|$]", "warning");
|
|
175
166
|
return;
|
|
176
167
|
}
|
|
177
|
-
// 币种变更后立即对齐注册表定价(CNY
|
|
178
|
-
|
|
168
|
+
// 币种变更后立即对齐注册表定价(CNY→覆盖为当前时段官方价;USD→还原 pi 内置价)
|
|
169
|
+
deepSeekPricing.applyPricingPatch(ctx.modelRegistry);
|
|
179
170
|
},
|
|
180
171
|
});
|
|
181
172
|
|
|
@@ -183,11 +174,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
183
174
|
pi.on("model_select", (event) => {
|
|
184
175
|
if (lastCtx) {
|
|
185
176
|
lastModel = event.model;
|
|
186
|
-
|
|
177
|
+
deepSeekPricing.applyPricingPatch(lastCtx.modelRegistry);
|
|
187
178
|
void refreshUsage(providers, lastCtx.modelRegistry, event.model);
|
|
188
179
|
}
|
|
189
180
|
});
|
|
190
181
|
|
|
182
|
+
// ---- Session shutdown: drop usage cache so /resume into a different provider
|
|
183
|
+
// shows fresh data instead of the previous session's cached segment.
|
|
184
|
+
pi.on("session_shutdown", () => {
|
|
185
|
+
resetUsageCache();
|
|
186
|
+
});
|
|
187
|
+
|
|
191
188
|
// ---- Footer ----
|
|
192
189
|
pi.on("session_start", (_event, ctx) => {
|
|
193
190
|
if (!ctx.hasUI) return;
|
|
@@ -197,7 +194,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
197
194
|
lastTps = 0;
|
|
198
195
|
|
|
199
196
|
// 根治:运行时修正 deepseek 定价(pi 内置定价低估约 7 倍),使后续 usage.cost 记录全局正确
|
|
200
|
-
|
|
197
|
+
deepSeekPricing.applyPricingPatch(ctx.modelRegistry);
|
|
201
198
|
|
|
202
199
|
// Prime usage cache on startup (non-blocking)
|
|
203
200
|
lastCtx = ctx;
|
|
@@ -211,6 +208,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
211
208
|
dispose: unsub,
|
|
212
209
|
invalidate() {},
|
|
213
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
|
+
}
|
|
214
216
|
const stats = tokenCalculator.compute(ctx);
|
|
215
217
|
const cu = ctx.getContextUsage();
|
|
216
218
|
const model = ctx.model;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fyeeme/pi-statusline",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Rich custom status bar for pi with token usage, cost, DeepSeek balance, context window,
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
7
7
|
"author": "fyeeme",
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
"deepseek",
|
|
28
28
|
"tokens",
|
|
29
29
|
"cost",
|
|
30
|
-
"mcp",
|
|
31
30
|
"balance",
|
|
32
31
|
"footer"
|
|
33
32
|
],
|
|
34
33
|
"files": [
|
|
35
34
|
"*.ts",
|
|
36
35
|
"providers/*.ts",
|
|
36
|
+
"pricing/*.ts",
|
|
37
37
|
"README.md",
|
|
38
38
|
"LICENSE",
|
|
39
39
|
"CHANGELOG.md"
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
"typecheck": "tsc"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
52
|
-
"@earendil-works/pi-ai": ">=0.
|
|
53
|
-
"@earendil-works/pi-tui": ">=0.
|
|
51
|
+
"@earendil-works/pi-coding-agent": ">=0.84.1",
|
|
52
|
+
"@earendil-works/pi-ai": ">=0.84.1",
|
|
53
|
+
"@earendil-works/pi-tui": ">=0.84.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
57
|
-
"@earendil-works/pi-ai": "0.
|
|
58
|
-
"@earendil-works/pi-tui": "0.
|
|
56
|
+
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
57
|
+
"@earendil-works/pi-ai": "0.84.1",
|
|
58
|
+
"@earendil-works/pi-tui": "0.84.1",
|
|
59
59
|
"@types/node": "22.19.19",
|
|
60
60
|
"typescript": "5.9.3"
|
|
61
61
|
}
|
|
@@ -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();
|
package/pricing/index.ts
ADDED
|
@@ -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
|
+
}
|
package/pricing/types.ts
ADDED
|
@@ -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
|
+
}
|
package/providers/deepseek.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
45
|
-
// 探测到币种后立即重新对齐注册表定价:session_start
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
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/providers/zai.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { fmt, formatCountdown, formatWeeklyCountdown } from "../footer.ts";
|
|
|
5
5
|
import { startOfCurrentWeekLocal, startOfNextWeekLocal } from "../week.ts";
|
|
6
6
|
|
|
7
7
|
/** Check if the provider name is a ZAI/GLM variant. */
|
|
8
|
-
function isZaiProvider(provider: string):
|
|
8
|
+
function isZaiProvider(provider: string): provider is "zai" | "zai-coding-cn" {
|
|
9
9
|
return provider === "zai" || provider === "zai-coding-cn";
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -84,7 +84,7 @@ export class ZaiUsageProvider implements UsageProvider {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
return {
|
|
87
|
-
provider:
|
|
87
|
+
provider: model.provider,
|
|
88
88
|
tokensLimitPct: fiveHourLimit?.percentage ?? 0,
|
|
89
89
|
tokensResetAt: fiveHourLimit?.nextResetTime ?? 0,
|
|
90
90
|
level,
|
|
@@ -99,7 +99,7 @@ export class ZaiUsageProvider implements UsageProvider {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
formatForFooter(result: NonNullable<ProviderUsageResult>, _sessionCost: number, _currency: string): string {
|
|
102
|
-
if (result.provider
|
|
102
|
+
if (!isZaiProvider(result.provider)) return "";
|
|
103
103
|
const zai = result as ZaiResult;
|
|
104
104
|
const parts: string[] = [];
|
|
105
105
|
|
|
@@ -121,7 +121,7 @@ export class ZaiUsageProvider implements UsageProvider {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
debugDump(result: NonNullable<ProviderUsageResult>, w: (s: string) => void): void {
|
|
124
|
-
if (result.provider
|
|
124
|
+
if (!isZaiProvider(result.provider)) return;
|
|
125
125
|
const zai = result as ZaiResult;
|
|
126
126
|
w(` tokensLimitPct: ${zai.tokensLimitPct}%`);
|
|
127
127
|
w(` tokensResetAt: ${new Date(zai.tokensResetAt).toISOString()}`);
|
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 {
|
|
4
|
+
import { getPricingStrategy } from "./pricing/index.ts";
|
|
5
5
|
|
|
6
6
|
export class SessionTokenUsageCalculator implements TokenUsageCalculator {
|
|
7
7
|
private getCurrencyOverride: () => "\u00a5" | "$" | undefined;
|
|
@@ -18,9 +18,13 @@ export class SessionTokenUsageCalculator implements TokenUsageCalculator {
|
|
|
18
18
|
let total = 0;
|
|
19
19
|
let cost = 0;
|
|
20
20
|
|
|
21
|
-
|
|
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
|
|
25
|
+
// sibling branches after /fork or /tree navigation. getEntries() would sum
|
|
26
|
+
// the whole tree and overcount in forked sessions.
|
|
27
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
24
28
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
25
29
|
const m = entry.message as AssistantMessage;
|
|
26
30
|
input += m.usage.input;
|
|
@@ -28,9 +32,12 @@ export class SessionTokenUsageCalculator implements TokenUsageCalculator {
|
|
|
28
32
|
cacheRead += m.usage.cacheRead;
|
|
29
33
|
cacheWrite += m.usage.cacheWrite;
|
|
30
34
|
total += m.usage.totalTokens;
|
|
31
|
-
// DeepSeek
|
|
32
|
-
|
|
33
|
-
|
|
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);
|
|
34
41
|
}
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -41,8 +48,7 @@ export class SessionTokenUsageCalculator implements TokenUsageCalculator {
|
|
|
41
48
|
if (override) {
|
|
42
49
|
currency = override;
|
|
43
50
|
} else {
|
|
44
|
-
|
|
45
|
-
if (p.toLowerCase().includes("deepseek")) currency = deepSeekBilledInCny() ? "\u00a5" : "$";
|
|
51
|
+
currency = strategy?.defaultCurrency() ?? "$";
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
return { input, output, cacheRead, cacheWrite, total, hitRate, cost, currency };
|
package/types.ts
CHANGED
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
|
-
}
|