@fyeeme/pi-statusline 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +80 -19
- package/cache.ts +7 -0
- package/cost.ts +135 -0
- package/index.ts +34 -22
- package/package.json +8 -9
- package/providers/deepseek.ts +8 -0
- package/providers/zai.ts +4 -4
- package/token-usage.ts +11 -3
- package/types.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.1] - 2026-08-08
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- 会话级 token/cost/tok-s 在分叉会话(`/fork`、`/tree`)后不再把整棵树计入:`SessionTokenUsageCalculator.compute` 与 `agent_end` 累计输出改用 `sessionManager.getBranch()`(仅当前分支);`/status-debug` 仍保留全量 `getEntries()` 用于诊断。
|
|
15
|
+
- 切换 provider(`/model` deepseek↔zai)后状态栏不再短暂显示上一个 provider 的用量段:`getCachedUsage` 在缓存 provider 与当前 provider 不匹配时返回 null,等待 `model_select` 触发的新刷新落地。
|
|
16
|
+
- ZAI 用量段对 `zai-coding-cn` provider 不再永久漏显示:`ZaiResult.provider` 记录实际 provider key(`zai`/`zai-coding-cn`),provider guard 不再因字面量 `"zai"` 误判(code-review #1)。
|
|
17
|
+
- tok/s 改用本次 agent run 的 output/耗时(`event.messages`),分子分母同口径,不再受 /fork 放弃分支的累计耗时影响;同时消除 `agent_end` 对模块级 `lastCtx` 的依赖(code-review #2)。
|
|
18
|
+
- `/status-debug` 的 msg 遍历改用 `getBranch()`,与 `tokenCalculator.compute` 口径一致(code-review #7)。
|
|
19
|
+
- `SessionTokenUsageCalculator.compute` 内 `deepSeekBilledInCny()` 冗余的第二次调用复用已有变量(code-review #8)。
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- 移除 `package.json` 中已废弃的 MCP 描述与 keyword(MCP 状态段自 1.0.2 起已移除)。
|
|
24
|
+
- 新增 `session_shutdown` 清理 usage cache:`/resume` 或 `/new` 进入不同 provider 时立即刷新,而非沿用上一会话缓存。
|
|
25
|
+
- `devDependencies` 对齐当前平台(`@earendil-works/pi-coding-agent`/`pi-ai`/`pi-tui` `0.77.0` → `0.84.1`),便于利用 `thinking_level_select`、`getAvailableProviderCount` 等新类型。
|
|
26
|
+
- README 状态栏示例 `deepseek-v3` → `deepseek-v4-pro`(`deepseek-v3` 已不在 0.84.x 模型库)。
|
|
27
|
+
|
|
28
|
+
## [1.1.0] - 2026-08-07
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- DeepSeek 结算币种感知定价(新模块 `cost.ts`):CNY 计费账户按官方 ¥ 单价重算(flash ¥1/¥2/¥0.02、pro ¥3/¥6/¥0.025 每百万 tokens),修正 pi 内置 USD 价低估约 7 倍;USD 计费账户保持 pi 内置价。
|
|
33
|
+
- balance API 自动探测账户结算币种;`/currency` 手动覆盖(auto/¥/$),切换后即时刷新注册表定价。
|
|
34
|
+
- 币种判定后即时对齐运行时注册表定价(覆盖与还原均可逆、跟随结算币种)。
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- USD 计费账户启动后不再整场会话停在 CNY 价:balance 探测到币种后立即重新应用定价补丁。
|
|
39
|
+
- 会话内新增 deepseek 模型(编辑 `models.json` + reload)后再次应用定价补丁时,不再因陈旧快照 + `registerProvider` 整表替换丢弃新增模型。
|
|
40
|
+
|
|
10
41
|
## [1.0.2] - 2025-07-15
|
|
11
42
|
|
|
12
43
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,17 +1,55 @@
|
|
|
1
1
|
# pi-statusline
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@fyeeme/pi-statusline)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
A rich custom status bar for [pi](https://pi.dev) that replaces the default footer. Provider-aware: shows live account balance for **DeepSeek** and rolling quota for **GLM/ZAI**, plus session tokens, cost, context window, and timing.
|
|
4
7
|
|
|
5
8
|
## Features
|
|
6
9
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
+
- **Provider-aware usage** — special support for DeepSeek and GLM/ZAI (see [Provider Support](#provider-support))
|
|
11
|
+
- **Token usage**: input, output, cache read/write, total per session, with cache hit rate
|
|
12
|
+
- **Cost**: cumulative session cost with currency auto-detection (¥ for DeepSeek/CNY, $ otherwise)
|
|
10
13
|
- **Context window**: usage percentage and size
|
|
11
14
|
- **Timing**: elapsed time + tokens/sec for last response
|
|
12
|
-
- **MCP status**: connected server count and total tool count
|
|
13
15
|
- **Git branch**: current branch shown in cwd display
|
|
14
16
|
|
|
17
|
+
## Provider Support
|
|
18
|
+
|
|
19
|
+
This extension uses a provider-aware strategy: when the active model belongs to a supported provider, the status bar shows live account-level usage data fetched directly from that provider's API. Other providers fall back to session-scoped cost only.
|
|
20
|
+
|
|
21
|
+
### DeepSeek
|
|
22
|
+
|
|
23
|
+
Shows live **account balance** and **weekly token usage**.
|
|
24
|
+
|
|
25
|
+
| Segment | Example | Source |
|
|
26
|
+
|---------|---------|--------|
|
|
27
|
+
| Balance | `¥0.12/50.00` | `GET /user/balance` (cached 5 min) |
|
|
28
|
+
| Weekly tokens | `7d:1.2M` | Local session file scan (rolling 7 days) |
|
|
29
|
+
|
|
30
|
+
- Currency auto-detected as `¥` (CNY) from the balance API response
|
|
31
|
+
- Balance is fetched on startup and refreshed in the background; cached for 5 minutes
|
|
32
|
+
- Triggered when `model.provider === "deepseek"`
|
|
33
|
+
|
|
34
|
+
### GLM / ZAI
|
|
35
|
+
|
|
36
|
+
Shows **5-hour rolling quota** and **weekly quota** (or natural-week usage as fallback).
|
|
37
|
+
|
|
38
|
+
| Segment | Example | Source |
|
|
39
|
+
|---------|---------|--------|
|
|
40
|
+
| 5h quota | `Usage 42%(1h23m)` | `GET /api/monitor/usage/quota/limit` (`unit:3`) |
|
|
41
|
+
| Weekly quota | `W:35%(1.2M,3d4h)` | `quota/limit` (`unit:6`) + `model-usage` API |
|
|
42
|
+
| Natural week | `W:1.2M` | `model-usage` API (Mon 00:00 local → now) |
|
|
43
|
+
|
|
44
|
+
- **5-hour rolling window**: percentage used + countdown to reset
|
|
45
|
+
- **Weekly quota**: if the plan exposes a `unit:6` weekly limit, shows percentage, tokens, and reset countdown; otherwise falls back to real usage for the current natural week (host local timezone)
|
|
46
|
+
- **Account level**: also fetched (visible in `/status-debug`)
|
|
47
|
+
- Supports both `zai` (`api.z.ai`) and `zai-coding-cn` (`open.bigmodel.cn`) endpoints
|
|
48
|
+
|
|
49
|
+
### Other providers
|
|
50
|
+
|
|
51
|
+
Session-scoped cost only (`$0.12` or `¥0.12`), with no live account data.
|
|
52
|
+
|
|
15
53
|
## Install
|
|
16
54
|
|
|
17
55
|
Requires the [pi](https://pi.dev) CLI.
|
|
@@ -20,16 +58,16 @@ Requires the [pi](https://pi.dev) CLI.
|
|
|
20
58
|
|
|
21
59
|
```bash
|
|
22
60
|
# Global (user) install — available in every project
|
|
23
|
-
pi install
|
|
61
|
+
pi install @fyeeme/pi-statusline
|
|
24
62
|
|
|
25
63
|
# Project-local — written to .pi/settings.json, shareable with your team
|
|
26
|
-
pi install -l
|
|
64
|
+
pi install -l @fyeeme/pi-statusline
|
|
27
65
|
|
|
28
66
|
# Pinned version — skipped by `pi update`
|
|
29
|
-
pi install
|
|
67
|
+
pi install @fyeeme/pi-statusline@1.0.2
|
|
30
68
|
|
|
31
69
|
# Try it once without saving (current run only)
|
|
32
|
-
pi -e
|
|
70
|
+
pi -e @fyeeme/pi-statusline
|
|
33
71
|
```
|
|
34
72
|
|
|
35
73
|
### From GitHub
|
|
@@ -40,7 +78,7 @@ Source: [`fyeeme/pi-packages`](https://github.com/fyeeme/pi-packages).
|
|
|
40
78
|
# HTTPS shorthand
|
|
41
79
|
pi install git:github.com/fyeeme/pi-packages
|
|
42
80
|
# Pin to a tag or commit (skipped by `pi update`)
|
|
43
|
-
pi install git:github.com/fyeeme/pi-packages@v1.0.
|
|
81
|
+
pi install git:github.com/fyeeme/pi-packages@v1.0.2
|
|
44
82
|
# Raw URL form
|
|
45
83
|
pi install https://github.com/fyeeme/pi-packages
|
|
46
84
|
```
|
|
@@ -51,20 +89,43 @@ See the Pi Packages guide on [pi.dev](https://pi.dev) for the full list of sourc
|
|
|
51
89
|
|
|
52
90
|
| Command | Description |
|
|
53
91
|
|---|---|
|
|
54
|
-
| `/
|
|
55
|
-
| `/
|
|
56
|
-
| `/status-debug` | Dump session stats to `/tmp/pi-status-debug.log` |
|
|
92
|
+
| `/currency [auto\|¥\|$]` | Toggle cost currency display (`auto` follows DeepSeek → ¥) |
|
|
93
|
+
| `/status-debug` | Dump session stats + provider usage to `/tmp/pi-status-debug.log` |
|
|
57
94
|
|
|
58
95
|
## Status Bar Layout
|
|
59
96
|
|
|
60
97
|
```
|
|
61
|
-
~/projects/my-repo (main) deepseek-
|
|
62
|
-
in 12k, out 8k, cache 45k
|
|
98
|
+
~/projects/my-repo (main) deepseek-v4-pro · xhigh
|
|
99
|
+
tokens 65k(in 12k, out 8k, cache 45k) · ¥0.12/50.00 · 7d:1.2M · 45.2%/64k · 2m30s 38.2tok/s
|
|
63
100
|
```
|
|
64
101
|
|
|
65
|
-
Line 1
|
|
66
|
-
|
|
102
|
+
**Line 1**: cwd + git branch (left) | model + thinking level (right)
|
|
103
|
+
|
|
104
|
+
**Line 2**: token stats · provider usage · context window · timing
|
|
105
|
+
|
|
106
|
+
### GLM/ZAI example
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
~/projects/my-repo (main) glm-4.6 · high
|
|
110
|
+
tokens 65k(in 12k, out 8k, cache 45k) · Usage 42%(1h23m) · W:35%(1.2M,3d4h) · 45.2%/64k · 2m30s 38.2tok/s
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Environment Variables
|
|
114
|
+
|
|
115
|
+
None required. API keys are resolved automatically by pi's model registry for the active provider.
|
|
116
|
+
|
|
117
|
+
## How It Works
|
|
118
|
+
|
|
119
|
+
The extension registers a `UsageProvider` per supported provider and refreshes usage data in the background (on startup, on model switch, and after each agent run). Results are cached with a 5-minute TTL (DeepSeek) or per-refresh (ZAI) to avoid hammering the provider APIs.
|
|
120
|
+
|
|
121
|
+
All network calls are best-effort with a 5-second timeout — failures stay silent and never block the status bar.
|
|
122
|
+
|
|
123
|
+
> **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.
|
|
124
|
+
|
|
125
|
+
## Changelog
|
|
126
|
+
|
|
127
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
67
128
|
|
|
68
|
-
##
|
|
129
|
+
## License
|
|
69
130
|
|
|
70
|
-
|
|
131
|
+
MIT
|
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/cost.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
}
|
package/index.ts
CHANGED
|
@@ -22,7 +22,12 @@ 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 {
|
|
25
|
+
import {
|
|
26
|
+
applyDeepSeekPricingPatch,
|
|
27
|
+
setCostCurrencyOverride,
|
|
28
|
+
getCostCurrencyOverride,
|
|
29
|
+
} from "./cost.ts";
|
|
30
|
+
import { refreshUsage, getCachedUsage, getUsageCacheAge, resetUsageCache } from "./cache.ts";
|
|
26
31
|
import { formatCwd, buildStatLine, buildInfoLine } from "./footer.ts";
|
|
27
32
|
|
|
28
33
|
// ---------------------------------------------------------------------------
|
|
@@ -45,9 +50,8 @@ let lastModel: ExtensionContext["model"] = undefined;
|
|
|
45
50
|
let agentRunning = false;
|
|
46
51
|
let lastElapsedSec = 0;
|
|
47
52
|
let lastTps = 0;
|
|
48
|
-
let currencyOverride: "¥" | "$" | undefined = undefined;
|
|
49
53
|
|
|
50
|
-
const tokenCalculator = new SessionTokenUsageCalculator(() =>
|
|
54
|
+
const tokenCalculator = new SessionTokenUsageCalculator(() => getCostCurrencyOverride());
|
|
51
55
|
|
|
52
56
|
// ---------------------------------------------------------------------------
|
|
53
57
|
// Helpers
|
|
@@ -75,21 +79,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
75
79
|
if (elapsedMs <= 0) return;
|
|
76
80
|
lastElapsedSec += elapsedMs / 1000;
|
|
77
81
|
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
} else {
|
|
87
|
-
// fallback: use current turn's messages when no session context
|
|
88
|
-
for (const m of event.messages) {
|
|
89
|
-
if (m.role === "assistant") cumulativeOutput += (m as AssistantMessage).usage.output;
|
|
90
|
-
}
|
|
82
|
+
// tok/s = 本次 agent run 的 output / 本次 run 耗时(即时速度)。用 event.messages
|
|
83
|
+
// 而非 sessionManager 遍历:不依赖 lastCtx/分支语义,不受 /fork 放弃分支的耗时
|
|
84
|
+
// 累计影响(分子分母同为本 run 口径),也避开 lastCtx 在 session 切换边界的 staleness。
|
|
85
|
+
let turnOutput = 0;
|
|
86
|
+
for (const m of event.messages) {
|
|
87
|
+
if (m.role === "assistant") turnOutput += (m as AssistantMessage).usage.output;
|
|
91
88
|
}
|
|
92
|
-
lastTps =
|
|
89
|
+
lastTps = turnOutput > 0 ? turnOutput / (elapsedMs / 1000) : 0;
|
|
93
90
|
|
|
94
91
|
// Schedule background refresh to pick up new usage data
|
|
95
92
|
if (lastCtx && lastModel) {
|
|
@@ -128,8 +125,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
128
125
|
const cacheAge = getUsageCacheAge();
|
|
129
126
|
w(` cacheAge: ${cacheAge !== null ? `${Math.round(cacheAge / 1000)}s` : "N/A"}`);
|
|
130
127
|
|
|
128
|
+
// status-debug dumps the current branch (same scope tokenCalculator.compute
|
|
129
|
+
// uses via getBranch), so the msg count (idx) lines up with computed totals.
|
|
131
130
|
let idx = 0;
|
|
132
|
-
for (const entry of ctx.sessionManager.
|
|
131
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
133
132
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
134
133
|
const m = entry.message as AssistantMessage;
|
|
135
134
|
w(`--- msg[${idx}] ---`);
|
|
@@ -158,17 +157,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
158
157
|
handler: async (args, ctx) => {
|
|
159
158
|
const a = args.trim().toLowerCase();
|
|
160
159
|
if (a === "auto" || a === "") {
|
|
161
|
-
|
|
162
|
-
ctx.ui.notify("Currency: auto (deepseek→¥)", "info");
|
|
160
|
+
setCostCurrencyOverride(undefined);
|
|
161
|
+
ctx.ui.notify("Currency: auto (deepseek→¥ or balance currency)", "info");
|
|
163
162
|
} else if (a === "¥" || a === "rmb" || a === "cny") {
|
|
164
|
-
|
|
163
|
+
setCostCurrencyOverride("¥");
|
|
165
164
|
ctx.ui.notify("Currency: ¥", "info");
|
|
166
165
|
} else if (a === "$" || a === "usd") {
|
|
167
|
-
|
|
166
|
+
setCostCurrencyOverride("$");
|
|
168
167
|
ctx.ui.notify("Currency: $", "info");
|
|
169
168
|
} else {
|
|
170
169
|
ctx.ui.notify("Usage: /currency [auto|¥|$]", "warning");
|
|
170
|
+
return;
|
|
171
171
|
}
|
|
172
|
+
// 币种变更后立即对齐注册表定价(CNY→覆盖为官方价;USD→还原 pi 内置价)
|
|
173
|
+
applyDeepSeekPricingPatch(ctx.modelRegistry);
|
|
172
174
|
},
|
|
173
175
|
});
|
|
174
176
|
|
|
@@ -176,10 +178,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
176
178
|
pi.on("model_select", (event) => {
|
|
177
179
|
if (lastCtx) {
|
|
178
180
|
lastModel = event.model;
|
|
181
|
+
applyDeepSeekPricingPatch(lastCtx.modelRegistry);
|
|
179
182
|
void refreshUsage(providers, lastCtx.modelRegistry, event.model);
|
|
180
183
|
}
|
|
181
184
|
});
|
|
182
185
|
|
|
186
|
+
// ---- Session shutdown: drop usage cache so /resume into a different provider
|
|
187
|
+
// shows fresh data instead of the previous session's cached segment.
|
|
188
|
+
pi.on("session_shutdown", () => {
|
|
189
|
+
resetUsageCache();
|
|
190
|
+
});
|
|
191
|
+
|
|
183
192
|
// ---- Footer ----
|
|
184
193
|
pi.on("session_start", (_event, ctx) => {
|
|
185
194
|
if (!ctx.hasUI) return;
|
|
@@ -188,6 +197,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
188
197
|
lastElapsedSec = 0;
|
|
189
198
|
lastTps = 0;
|
|
190
199
|
|
|
200
|
+
// 根治:运行时修正 deepseek 定价(pi 内置定价低估约 7 倍),使后续 usage.cost 记录全局正确
|
|
201
|
+
applyDeepSeekPricingPatch(ctx.modelRegistry);
|
|
202
|
+
|
|
191
203
|
// Prime usage cache on startup (non-blocking)
|
|
192
204
|
lastCtx = ctx;
|
|
193
205
|
lastModel = 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.1.1",
|
|
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,7 +27,6 @@
|
|
|
27
27
|
"deepseek",
|
|
28
28
|
"tokens",
|
|
29
29
|
"cost",
|
|
30
|
-
"mcp",
|
|
31
30
|
"balance",
|
|
32
31
|
"footer"
|
|
33
32
|
],
|
|
@@ -48,14 +47,14 @@
|
|
|
48
47
|
"typecheck": "tsc"
|
|
49
48
|
},
|
|
50
49
|
"peerDependencies": {
|
|
51
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
52
|
-
"@earendil-works/pi-ai": ">=0.
|
|
53
|
-
"@earendil-works/pi-tui": ">=0.
|
|
50
|
+
"@earendil-works/pi-coding-agent": ">=0.84.1",
|
|
51
|
+
"@earendil-works/pi-ai": ">=0.84.1",
|
|
52
|
+
"@earendil-works/pi-tui": ">=0.84.1"
|
|
54
53
|
},
|
|
55
54
|
"devDependencies": {
|
|
56
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
57
|
-
"@earendil-works/pi-ai": "0.
|
|
58
|
-
"@earendil-works/pi-tui": "0.
|
|
55
|
+
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
56
|
+
"@earendil-works/pi-ai": "0.84.1",
|
|
57
|
+
"@earendil-works/pi-tui": "0.84.1",
|
|
59
58
|
"@types/node": "22.19.19",
|
|
60
59
|
"typescript": "5.9.3"
|
|
61
60
|
}
|
package/providers/deepseek.ts
CHANGED
|
@@ -1,6 +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
5
|
import { scanWeeklyTokens } from "../session-scanner.ts";
|
|
5
6
|
import { fmt } from "../footer.ts";
|
|
6
7
|
|
|
@@ -39,6 +40,13 @@ export class DeepSeekUsageProvider implements UsageProvider {
|
|
|
39
40
|
const info = data.balance_infos?.[0];
|
|
40
41
|
if (!info) return null;
|
|
41
42
|
|
|
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);
|
|
49
|
+
|
|
42
50
|
const weeklyTokens = scanWeeklyTokens("deepseek");
|
|
43
51
|
|
|
44
52
|
return {
|
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,6 +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
5
|
|
|
5
6
|
export class SessionTokenUsageCalculator implements TokenUsageCalculator {
|
|
6
7
|
private getCurrencyOverride: () => "\u00a5" | "$" | undefined;
|
|
@@ -17,7 +18,12 @@ export class SessionTokenUsageCalculator implements TokenUsageCalculator {
|
|
|
17
18
|
let total = 0;
|
|
18
19
|
let cost = 0;
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
const billedInCny = deepSeekBilledInCny();
|
|
22
|
+
|
|
23
|
+
// Use getBranch() (current branch only) so token/cost stats exclude abandoned
|
|
24
|
+
// sibling branches after /fork or /tree navigation. getEntries() would sum
|
|
25
|
+
// the whole tree and overcount in forked sessions.
|
|
26
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
21
27
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
22
28
|
const m = entry.message as AssistantMessage;
|
|
23
29
|
input += m.usage.input;
|
|
@@ -25,7 +31,9 @@ export class SessionTokenUsageCalculator implements TokenUsageCalculator {
|
|
|
25
31
|
cacheRead += m.usage.cacheRead;
|
|
26
32
|
cacheWrite += m.usage.cacheWrite;
|
|
27
33
|
total += m.usage.totalTokens;
|
|
28
|
-
|
|
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);
|
|
29
37
|
}
|
|
30
38
|
}
|
|
31
39
|
|
|
@@ -37,7 +45,7 @@ export class SessionTokenUsageCalculator implements TokenUsageCalculator {
|
|
|
37
45
|
currency = override;
|
|
38
46
|
} else {
|
|
39
47
|
const p = ctx.model?.provider ?? "";
|
|
40
|
-
if (p.toLowerCase().includes("deepseek")) currency = "\u00a5";
|
|
48
|
+
if (p.toLowerCase().includes("deepseek")) currency = billedInCny ? "\u00a5" : "$";
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
return { input, output, cacheRead, cacheWrite, total, hitRate, cost, currency };
|