@hk_net/pi-usage-bars 0.3.0 → 0.4.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 +23 -0
- package/README.md +40 -5
- package/docs/financial-metrics-plan.md +75 -0
- package/docs/releasing.md +106 -0
- package/extensions/usage-bars/core.ts +590 -3
- package/extensions/usage-bars/index.ts +159 -20
- package/package.json +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-07-22
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added Kimi For Coding quota support through Pi's `kimi-coding` credential and the first-party five-hour/weekly usage endpoint.
|
|
8
|
+
- Added separate MiniMax Global and China Coding/Token Plan support, including the current token-plan endpoint and legacy coding-plan fallback.
|
|
9
|
+
- Added provider-specific quota labels and support for MiniMax responses that expose an interval quota without a weekly quota.
|
|
10
|
+
- Added neutral MiniMax purchased-Credits balance rendering when a key-authenticated first-party response exposes a balance.
|
|
11
|
+
- Added OpenRouter account balance and daily/weekly/monthly key spend using the first-party Credits and Key APIs.
|
|
12
|
+
- Added OpenRouter per-key limit bars only when the key has a real configured credit limit.
|
|
13
|
+
- Added DeepSeek total, topped-up, and granted balance support through the official key-authenticated balance API.
|
|
14
|
+
- Added separate Moonshot/Kimi API Global and China available, cash, and voucher balance support.
|
|
15
|
+
- Added a typed financial-metrics roadmap for further balance/spend providers.
|
|
16
|
+
- Added provider parsing, regional routing, endpoint fallback, financial rendering, authentication lifecycle, and smoke tests.
|
|
17
|
+
- Added CI, reproducible installs, production auditing, and a maintainer release guide.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Treat MiniMax status `2062` as a neutral “No active Token Plan” account state rather than an API error; the cookie-only console balance endpoint remains out of scope.
|
|
22
|
+
- Expanded endpoint configuration documentation for Kimi, MiniMax, OpenRouter, DeepSeek, and Moonshot.
|
|
23
|
+
- Changed npm publishing to an explicit manual workflow so source tags cannot accidentally republish an already released version.
|
|
24
|
+
- Bumped the package version to 0.4.0.
|
|
25
|
+
|
|
3
26
|
## [0.3.0] - 2026-07-22
|
|
4
27
|
|
|
5
28
|
### Breaking changes
|
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# pi-usage-bars
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Quota, balance, and spend indicators for [Pi](https://github.com/earendil-works/pi).
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
7
|
It adds:
|
|
8
8
|
|
|
9
|
-
- a footer status bar for the active
|
|
10
|
-
- a `/usage` command showing
|
|
9
|
+
- a footer status bar for the active supported provider
|
|
10
|
+
- a `/usage` command showing configured quota, balance, and spend data
|
|
11
11
|
|
|
12
12
|
## Supported providers
|
|
13
13
|
|
|
@@ -17,6 +17,19 @@ It adds:
|
|
|
17
17
|
| Anthropic Claude | `anthropic` | Claude Pro/Max OAuth |
|
|
18
18
|
| ZAI Coding Plan (Global) | `zai` | ZAI API key |
|
|
19
19
|
| ZAI Coding Plan (China) | `zai-coding-cn` | ZAI China API key |
|
|
20
|
+
| Kimi For Coding | `kimi-coding` | Kimi Coding Plan API key |
|
|
21
|
+
| MiniMax Coding Plan (Global) | `minimax` | MiniMax Global API key |
|
|
22
|
+
| MiniMax Coding Plan (China) | `minimax-cn` | MiniMax China API key |
|
|
23
|
+
| OpenRouter | `openrouter` | OpenRouter API key |
|
|
24
|
+
| DeepSeek | `deepseek` | DeepSeek API key |
|
|
25
|
+
| Moonshot/Kimi API (Global) | `moonshotai` | Moonshot Global API key |
|
|
26
|
+
| Moonshot/Kimi API (China) | `moonshotai-cn` | Moonshot China API key |
|
|
27
|
+
|
|
28
|
+
DeepSeek shows total, topped-up, and granted balances in the currency returned by the API. Moonshot shows available, cash, and voucher balances; this is separate from the Kimi For Coding subscription provider. Pi uses `MOONSHOT_API_KEY` for both Moonshot regions, so `/usage` automatically hides the expected authentication failure from the region where a shared environment key is not valid.
|
|
29
|
+
|
|
30
|
+
OpenRouter shows the account credit balance and current daily, weekly, and monthly key spend. If the API key has a configured credit limit, that limit is also rendered as a usage bar.
|
|
31
|
+
|
|
32
|
+
MiniMax Subscription Keys can represent an active Token Plan, purchased Credits, or both. The extension shows quota windows when present and a neutral credit-balance line if a first-party key-authenticated response exposes `points_balance`/`credits_balance`. MiniMax currently exposes Credits-only balances through a console endpoint requiring browser-cookie authentication, so a key-only Credits account is shown as “No active Token Plan” with a direction to check the console rather than a fabricated percentage. The extension does not import browser cookies.
|
|
20
33
|
|
|
21
34
|
Google Gemini CLI and Google Antigravity are not supported because Pi removed those built-in providers in version 0.71.0.
|
|
22
35
|
|
|
@@ -43,11 +56,17 @@ pi install https://github.com/hknet/pi-usage-bars
|
|
|
43
56
|
|
|
44
57
|
Restart Pi after installation, or use `/reload` when the package is already installed.
|
|
45
58
|
|
|
59
|
+
Update an existing npm installation with:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pi update npm:@hk_net/pi-usage-bars
|
|
63
|
+
```
|
|
64
|
+
|
|
46
65
|
## Use
|
|
47
66
|
|
|
48
67
|
### Footer usage bars
|
|
49
68
|
|
|
50
|
-
When the active model belongs to a configured supported provider, the footer shows
|
|
69
|
+
When the active model belongs to a configured supported provider, the footer shows its available quota windows with reset countdowns when provided by the service.
|
|
51
70
|
|
|
52
71
|

|
|
53
72
|
|
|
@@ -61,17 +80,31 @@ Run `/usage` to open a searchable list of configured supported providers.
|
|
|
61
80
|
|
|
62
81
|
## Endpoint configuration
|
|
63
82
|
|
|
64
|
-
|
|
83
|
+
First-party monitoring endpoints can be overridden:
|
|
65
84
|
|
|
66
85
|
| Variable | Default |
|
|
67
86
|
| --- | --- |
|
|
68
87
|
| `PI_ZAI_USAGE_ENDPOINT` | `https://api.z.ai/api/monitor/usage/quota/limit` |
|
|
69
88
|
| `PI_ZAI_CODING_CN_USAGE_ENDPOINT` | `https://open.bigmodel.cn/api/monitor/usage/quota/limit` |
|
|
89
|
+
| `PI_KIMI_USAGE_ENDPOINT` | `https://api.kimi.com/coding/v1/usages` |
|
|
90
|
+
| `PI_MINIMAX_USAGE_ENDPOINT` | `https://api.minimax.io/v1/token_plan/remains` |
|
|
91
|
+
| `PI_MINIMAX_LEGACY_USAGE_ENDPOINT` | `https://api.minimax.io/v1/api/openplatform/coding_plan/remains` |
|
|
92
|
+
| `PI_MINIMAX_CN_USAGE_ENDPOINT` | `https://api.minimaxi.com/v1/token_plan/remains` |
|
|
93
|
+
| `PI_MINIMAX_CN_LEGACY_USAGE_ENDPOINT` | `https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains` |
|
|
94
|
+
| `PI_OPENROUTER_CREDITS_ENDPOINT` | `https://openrouter.ai/api/v1/credits` |
|
|
95
|
+
| `PI_OPENROUTER_KEY_ENDPOINT` | `https://openrouter.ai/api/v1/key` |
|
|
96
|
+
| `PI_DEEPSEEK_BALANCE_ENDPOINT` | `https://api.deepseek.com/user/balance` |
|
|
97
|
+
| `PI_MOONSHOT_BALANCE_ENDPOINT` | `https://api.moonshot.ai/v1/users/me/balance` |
|
|
98
|
+
| `PI_MOONSHOT_CN_BALANCE_ENDPOINT` | `https://api.moonshot.cn/v1/users/me/balance` |
|
|
70
99
|
|
|
71
100
|
**Security:** the corresponding provider token is sent as a bearer token to the configured endpoint. Only override these variables with an endpoint you trust.
|
|
72
101
|
|
|
73
102
|
The Codex and Claude usage endpoints are fixed to their first-party services. Claude responses are cached briefly in the system temporary directory to coordinate multiple Pi processes and reduce rate limiting. The cache contains usage values, not credentials.
|
|
74
103
|
|
|
104
|
+
## Financial metrics roadmap
|
|
105
|
+
|
|
106
|
+
Quota percentages and monetary account data have different meaning and color semantics. OpenRouter, DeepSeek, Moonshot, and MiniMax financial data are rendered as neutral account values; percentages are used only when an actual limit exists. See [Financial metrics plan](docs/financial-metrics-plan.md) for the normalized model and future provider rollout.
|
|
107
|
+
|
|
75
108
|
## Development
|
|
76
109
|
|
|
77
110
|
Install Node.js 22.19+ and Bun 1.3, then run:
|
|
@@ -83,6 +116,8 @@ npm run check
|
|
|
83
116
|
|
|
84
117
|
`npm run check` performs a strict TypeScript check, runs the Bun test suite, and smoke-loads the extension with the pinned current Pi release.
|
|
85
118
|
|
|
119
|
+
Maintainers should follow the complete [release and npm publication guide](docs/releasing.md). npm publication is manual; pushing a Git tag does not publish automatically.
|
|
120
|
+
|
|
86
121
|
## Credits
|
|
87
122
|
|
|
88
123
|
This extension is based on and inspired by:
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Financial metrics plan
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Add an optional account-value view without treating balances and spend as subscription quota. Quota usage increases toward exhaustion; a balance decreases toward exhaustion; spend may be meaningful only relative to a budget. Those metrics must retain distinct labels and color rules.
|
|
6
|
+
|
|
7
|
+
## Normalized model
|
|
8
|
+
|
|
9
|
+
A future core revision should replace the two fixed quota fields with a list of typed metrics:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
type UsageMetric =
|
|
13
|
+
| {
|
|
14
|
+
kind: "quota";
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
usedPercent: number;
|
|
18
|
+
resetsAt?: string;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
kind: "money";
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
currency: string;
|
|
25
|
+
balance?: number;
|
|
26
|
+
spent?: number;
|
|
27
|
+
limit?: number;
|
|
28
|
+
period?: "day" | "week" | "month" | "lifetime";
|
|
29
|
+
resetsAt?: string;
|
|
30
|
+
};
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Provider fetchers should return normalized metrics. Rendering, not provider parsing, decides which metrics appear in the footer or `/usage` dialog.
|
|
34
|
+
|
|
35
|
+
## View behavior
|
|
36
|
+
|
|
37
|
+
The initial OpenRouter implementation displays both balance and spend in `/usage`. The footer prefers the balance and current-month spend, while a configured per-key limit remains the primary bar. A later persistent view setting may provide:
|
|
38
|
+
|
|
39
|
+
- `auto`: prefer quota windows; otherwise show a financial summary.
|
|
40
|
+
- `balance`: show remaining prepaid/credit balance.
|
|
41
|
+
- `spent`: show spend and a percentage only when a real limit or budget exists.
|
|
42
|
+
- `off`: retain quota-only behavior.
|
|
43
|
+
|
|
44
|
+
No environment variable or config key is reserved for that future view selector yet.
|
|
45
|
+
|
|
46
|
+
Color semantics:
|
|
47
|
+
|
|
48
|
+
- quota `usedPercent`: high is bad;
|
|
49
|
+
- balance: low remaining balance is bad only when a known starting balance or warning threshold exists;
|
|
50
|
+
- spend/limit: high is bad;
|
|
51
|
+
- unconstrained spend and raw balance: neutral colors, never a manufactured percentage.
|
|
52
|
+
|
|
53
|
+
## Provider rollout
|
|
54
|
+
|
|
55
|
+
1. **OpenRouter — implemented in 0.4.0**
|
|
56
|
+
- `GET /api/v1/credits`: purchased credits, total usage, derived balance.
|
|
57
|
+
- `GET /api/v1/key`: configured key limit and daily/weekly/monthly spend where supplied.
|
|
58
|
+
- Uses only the API key resolved through Pi's provider registry.
|
|
59
|
+
2. **Balance providers**
|
|
60
|
+
- DeepSeek and Moonshot/Kimi API prepaid balances are implemented in 0.4.0 using their official key-authenticated endpoints.
|
|
61
|
+
- Mistral balance or monthly-plan data when available through an API credential.
|
|
62
|
+
- MiniMax purchased Credits have a limited provider-specific implementation: when a first-party key-authenticated quota response includes `points_balance` or `credits_balance`, it is rendered neutrally. In current MiniMax deployments, the dedicated `/backend/account/token_plan_credit` endpoint requires browser-cookie authentication and does not accept the Subscription Key, so it remains out of scope. API status `2062` is rendered as a neutral “No active Token Plan” account state and never converted into a quota percentage.
|
|
63
|
+
3. **Spend providers**
|
|
64
|
+
- OpenAI organization spend requires an Admin API key and therefore should appear only when Pi can resolve an appropriate credential.
|
|
65
|
+
- AWS Bedrock spend requires explicit Cost Explorer permissions and should remain a separate opt-in integration.
|
|
66
|
+
|
|
67
|
+
Browser cookies and direct reads of Pi's credential files remain out of scope.
|
|
68
|
+
|
|
69
|
+
## Migration steps
|
|
70
|
+
|
|
71
|
+
1. Introduce `UsageMetric` alongside the existing `session`/`weekly` compatibility fields.
|
|
72
|
+
2. Convert quota fetchers and both renderers to metric lists.
|
|
73
|
+
3. Add persistent view selection; neutral financial formatting is already covered by OpenRouter tests.
|
|
74
|
+
4. Add further key-authenticated financial providers beyond OpenRouter, DeepSeek, and Moonshot.
|
|
75
|
+
5. Remove compatibility fields only in a documented major release.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
This repository uses semantic versions. The package version, changelog heading, Git tag, and npm version must agree.
|
|
4
|
+
|
|
5
|
+
The npm publish workflow is manual-only. Pushing a Git tag does not publish the package, which prevents a tag pushed after a local npm release from attempting to publish the same version twice.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Write access to `hknet/pi-usage-bars`.
|
|
10
|
+
- Publish access to the npm scope `@hk_net`.
|
|
11
|
+
- npm authentication for the `hk_net` account (`npm whoami`).
|
|
12
|
+
- Node.js 22.19 or newer.
|
|
13
|
+
- Bun 1.3.0. The commands below use a temporary pinned Bun installation, so a global Bun installation is not required.
|
|
14
|
+
|
|
15
|
+
Never place an npm token in the repository, command history, or endpoint override. Complete npm's browser or OTP authorization when prompted.
|
|
16
|
+
|
|
17
|
+
## 1. Verify the source release
|
|
18
|
+
|
|
19
|
+
From the repository root:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git switch main
|
|
23
|
+
git pull --ff-only origin main
|
|
24
|
+
git status --short
|
|
25
|
+
node -p "require('./package.json').version"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For this release, the version must be `0.4.0` and the working tree must be clean.
|
|
29
|
+
|
|
30
|
+
Run the same checks used by CI and npm's `prepublishOnly` hook:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx --yes --package bun@1.3.0 -- npm run check
|
|
34
|
+
npm audit --omit=dev --audit-level=high
|
|
35
|
+
npm pack --dry-run
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Expected results for `0.4.0`:
|
|
39
|
+
|
|
40
|
+
- strict TypeScript check passes;
|
|
41
|
+
- 32 tests pass;
|
|
42
|
+
- Pi smoke test passes;
|
|
43
|
+
- production audit has no vulnerabilities;
|
|
44
|
+
- the dry-run package is `@hk_net/pi-usage-bars@0.4.0` and contains the extension, documentation, changelog, and license.
|
|
45
|
+
|
|
46
|
+
## 2. Verify npm state
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm whoami
|
|
50
|
+
npm view @hk_net/pi-usage-bars version
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Before publishing `0.4.0`, the registry should still report `0.3.0`. If it already reports `0.4.0`, do not publish it again.
|
|
54
|
+
|
|
55
|
+
Optionally inspect exactly what npm will receive:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm pack --dry-run --json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 3. Publish manually
|
|
62
|
+
|
|
63
|
+
Local npm publication cannot use GitHub's OIDC provenance attestation, so explicitly disable provenance for this manual release:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx --yes --package bun@1.3.0 -- \
|
|
67
|
+
npm publish --access public --provenance=false
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The `prepublishOnly` hook reruns the complete check. Complete the npm browser/OTP authorization if requested. Do not retry blindly after an ambiguous network failure; verify the registry first.
|
|
71
|
+
|
|
72
|
+
## 4. Verify the published package
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm view @hk_net/pi-usage-bars version dist-tags.latest
|
|
76
|
+
npm view @hk_net/pi-usage-bars@0.4.0 dist.integrity
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Both version values should report `0.4.0`, and the integrity field should be present.
|
|
80
|
+
|
|
81
|
+
Test installation or update through Pi:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pi install npm:@hk_net/pi-usage-bars
|
|
85
|
+
# If it is already installed:
|
|
86
|
+
pi update npm:@hk_net/pi-usage-bars
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Restart Pi, or use `/reload`, then run `/usage`.
|
|
90
|
+
|
|
91
|
+
## 5. Tag the verified source
|
|
92
|
+
|
|
93
|
+
Only tag after npm verification succeeds:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git switch main
|
|
97
|
+
git pull --ff-only origin main
|
|
98
|
+
git tag -a v0.4.0 -m "Release v0.4.0"
|
|
99
|
+
git push origin v0.4.0
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The tag does not trigger npm publication. It records the exact source corresponding to the already-published package. A GitHub release can then be created from `v0.4.0` using the `0.4.0` changelog section.
|
|
103
|
+
|
|
104
|
+
## CI publishing alternative
|
|
105
|
+
|
|
106
|
+
The **Publish to npm (manual)** GitHub Actions workflow remains available as an alternative. It requires the `NPM_TOKEN` repository secret and publishes with npm provenance. Do not run it after publishing the same version locally.
|
|
@@ -2,18 +2,62 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
|
-
export type ProviderKey =
|
|
6
|
-
|
|
5
|
+
export type ProviderKey =
|
|
6
|
+
| "codex"
|
|
7
|
+
| "claude"
|
|
8
|
+
| "zai"
|
|
9
|
+
| "zai-cn"
|
|
10
|
+
| "kimi"
|
|
11
|
+
| "minimax"
|
|
12
|
+
| "minimax-cn"
|
|
13
|
+
| "openrouter"
|
|
14
|
+
| "deepseek"
|
|
15
|
+
| "moonshot"
|
|
16
|
+
| "moonshot-cn";
|
|
17
|
+
export type PiProviderId =
|
|
18
|
+
| "openai-codex"
|
|
19
|
+
| "anthropic"
|
|
20
|
+
| "zai"
|
|
21
|
+
| "zai-coding-cn"
|
|
22
|
+
| "kimi-coding"
|
|
23
|
+
| "minimax"
|
|
24
|
+
| "minimax-cn"
|
|
25
|
+
| "openrouter"
|
|
26
|
+
| "deepseek"
|
|
27
|
+
| "moonshotai"
|
|
28
|
+
| "moonshotai-cn";
|
|
29
|
+
|
|
30
|
+
export interface AccountBalance {
|
|
31
|
+
amount: number;
|
|
32
|
+
unit: string;
|
|
33
|
+
label: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AccountSpend {
|
|
37
|
+
unit: string;
|
|
38
|
+
daily?: number;
|
|
39
|
+
weekly?: number;
|
|
40
|
+
monthly?: number;
|
|
41
|
+
lifetime?: number;
|
|
42
|
+
}
|
|
7
43
|
|
|
8
44
|
export interface UsageData {
|
|
9
45
|
session: number;
|
|
10
46
|
weekly: number;
|
|
47
|
+
quotaHidden?: boolean;
|
|
48
|
+
accountBalance?: AccountBalance;
|
|
49
|
+
accountBalanceDetails?: AccountBalance[];
|
|
50
|
+
accountSpend?: AccountSpend;
|
|
11
51
|
sessionResetsIn?: string;
|
|
12
52
|
weeklyResetsIn?: string;
|
|
13
53
|
sessionResetsAt?: string;
|
|
14
54
|
weeklyResetsAt?: string;
|
|
15
55
|
extraSpend?: number;
|
|
16
56
|
extraLimit?: number;
|
|
57
|
+
sessionLabel?: string;
|
|
58
|
+
weeklyLabel?: string;
|
|
59
|
+
weeklyHidden?: boolean;
|
|
60
|
+
notice?: string;
|
|
17
61
|
warning?: string;
|
|
18
62
|
stale?: boolean;
|
|
19
63
|
fetchedAt?: number;
|
|
@@ -26,6 +70,16 @@ export type UsageTokens = Partial<Record<ProviderKey, string>>;
|
|
|
26
70
|
export interface UsageEndpoints {
|
|
27
71
|
zai: string;
|
|
28
72
|
zaiCn: string;
|
|
73
|
+
kimi: string;
|
|
74
|
+
minimax: string;
|
|
75
|
+
minimaxLegacy: string;
|
|
76
|
+
minimaxCn: string;
|
|
77
|
+
minimaxCnLegacy: string;
|
|
78
|
+
openRouterCredits: string;
|
|
79
|
+
openRouterKey: string;
|
|
80
|
+
deepSeekBalance: string;
|
|
81
|
+
moonshotBalance: string;
|
|
82
|
+
moonshotCnBalance: string;
|
|
29
83
|
}
|
|
30
84
|
|
|
31
85
|
export interface HeadersLike {
|
|
@@ -108,6 +162,16 @@ const CLAUDE_LOCK_STALE_MS = 20_000;
|
|
|
108
162
|
export const DEFAULT_USAGE_CACHE_FILE = path.join(os.tmpdir(), "pi", "usage-bars-cache.json");
|
|
109
163
|
export const DEFAULT_ZAI_USAGE_ENDPOINT = "https://api.z.ai/api/monitor/usage/quota/limit";
|
|
110
164
|
export const DEFAULT_ZAI_CN_USAGE_ENDPOINT = "https://open.bigmodel.cn/api/monitor/usage/quota/limit";
|
|
165
|
+
export const DEFAULT_KIMI_USAGE_ENDPOINT = "https://api.kimi.com/coding/v1/usages";
|
|
166
|
+
export const DEFAULT_MINIMAX_USAGE_ENDPOINT = "https://api.minimax.io/v1/token_plan/remains";
|
|
167
|
+
export const DEFAULT_MINIMAX_LEGACY_USAGE_ENDPOINT = "https://api.minimax.io/v1/api/openplatform/coding_plan/remains";
|
|
168
|
+
export const DEFAULT_MINIMAX_CN_USAGE_ENDPOINT = "https://api.minimaxi.com/v1/token_plan/remains";
|
|
169
|
+
export const DEFAULT_MINIMAX_CN_LEGACY_USAGE_ENDPOINT = "https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains";
|
|
170
|
+
export const DEFAULT_OPENROUTER_CREDITS_ENDPOINT = "https://openrouter.ai/api/v1/credits";
|
|
171
|
+
export const DEFAULT_OPENROUTER_KEY_ENDPOINT = "https://openrouter.ai/api/v1/key";
|
|
172
|
+
export const DEFAULT_DEEPSEEK_BALANCE_ENDPOINT = "https://api.deepseek.com/user/balance";
|
|
173
|
+
export const DEFAULT_MOONSHOT_BALANCE_ENDPOINT = "https://api.moonshot.ai/v1/users/me/balance";
|
|
174
|
+
export const DEFAULT_MOONSHOT_CN_BALANCE_ENDPOINT = "https://api.moonshot.cn/v1/users/me/balance";
|
|
111
175
|
|
|
112
176
|
export function resolveUsageEndpoints(env: NodeJS.ProcessEnv = process.env): UsageEndpoints {
|
|
113
177
|
const configured = (value: string | undefined, fallback: string) => {
|
|
@@ -118,6 +182,16 @@ export function resolveUsageEndpoints(env: NodeJS.ProcessEnv = process.env): Usa
|
|
|
118
182
|
return {
|
|
119
183
|
zai: configured(env.PI_ZAI_USAGE_ENDPOINT, DEFAULT_ZAI_USAGE_ENDPOINT),
|
|
120
184
|
zaiCn: configured(env.PI_ZAI_CODING_CN_USAGE_ENDPOINT, DEFAULT_ZAI_CN_USAGE_ENDPOINT),
|
|
185
|
+
kimi: configured(env.PI_KIMI_USAGE_ENDPOINT, DEFAULT_KIMI_USAGE_ENDPOINT),
|
|
186
|
+
minimax: configured(env.PI_MINIMAX_USAGE_ENDPOINT, DEFAULT_MINIMAX_USAGE_ENDPOINT),
|
|
187
|
+
minimaxLegacy: configured(env.PI_MINIMAX_LEGACY_USAGE_ENDPOINT, DEFAULT_MINIMAX_LEGACY_USAGE_ENDPOINT),
|
|
188
|
+
minimaxCn: configured(env.PI_MINIMAX_CN_USAGE_ENDPOINT, DEFAULT_MINIMAX_CN_USAGE_ENDPOINT),
|
|
189
|
+
minimaxCnLegacy: configured(env.PI_MINIMAX_CN_LEGACY_USAGE_ENDPOINT, DEFAULT_MINIMAX_CN_LEGACY_USAGE_ENDPOINT),
|
|
190
|
+
openRouterCredits: configured(env.PI_OPENROUTER_CREDITS_ENDPOINT, DEFAULT_OPENROUTER_CREDITS_ENDPOINT),
|
|
191
|
+
openRouterKey: configured(env.PI_OPENROUTER_KEY_ENDPOINT, DEFAULT_OPENROUTER_KEY_ENDPOINT),
|
|
192
|
+
deepSeekBalance: configured(env.PI_DEEPSEEK_BALANCE_ENDPOINT, DEFAULT_DEEPSEEK_BALANCE_ENDPOINT),
|
|
193
|
+
moonshotBalance: configured(env.PI_MOONSHOT_BALANCE_ENDPOINT, DEFAULT_MOONSHOT_BALANCE_ENDPOINT),
|
|
194
|
+
moonshotCnBalance: configured(env.PI_MOONSHOT_CN_BALANCE_ENDPOINT, DEFAULT_MOONSHOT_CN_BALANCE_ENDPOINT),
|
|
121
195
|
};
|
|
122
196
|
}
|
|
123
197
|
|
|
@@ -384,12 +458,20 @@ function snapshotUsage(usage: UsageData, nowMs = Date.now()): UsageData {
|
|
|
384
458
|
return {
|
|
385
459
|
session: usage.session,
|
|
386
460
|
weekly: usage.weekly,
|
|
461
|
+
quotaHidden: usage.quotaHidden,
|
|
462
|
+
accountBalance: usage.accountBalance,
|
|
463
|
+
accountBalanceDetails: usage.accountBalanceDetails,
|
|
464
|
+
accountSpend: usage.accountSpend,
|
|
387
465
|
sessionResetsAt: usage.sessionResetsAt,
|
|
388
466
|
weeklyResetsAt: usage.weeklyResetsAt,
|
|
389
467
|
sessionResetsIn: usage.sessionResetsIn,
|
|
390
468
|
weeklyResetsIn: usage.weeklyResetsIn,
|
|
391
469
|
extraSpend: usage.extraSpend,
|
|
392
470
|
extraLimit: usage.extraLimit,
|
|
471
|
+
sessionLabel: usage.sessionLabel,
|
|
472
|
+
weeklyLabel: usage.weeklyLabel,
|
|
473
|
+
weeklyHidden: usage.weeklyHidden,
|
|
474
|
+
notice: usage.notice,
|
|
393
475
|
fetchedAt: usage.fetchedAt ?? nowMs,
|
|
394
476
|
};
|
|
395
477
|
}
|
|
@@ -545,6 +627,463 @@ export async function fetchClaudeUsageWithFallback(
|
|
|
545
627
|
}
|
|
546
628
|
}
|
|
547
629
|
|
|
630
|
+
function readNumber(value: unknown): number | null {
|
|
631
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
632
|
+
if (typeof value === "string" && value.trim()) {
|
|
633
|
+
const parsed = Number(value);
|
|
634
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
635
|
+
}
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function usedPercentFromCounts(
|
|
640
|
+
value: Record<string, unknown> | null | undefined,
|
|
641
|
+
options: { remainingPercent?: string; used?: string; total?: string; remaining?: string } = {},
|
|
642
|
+
): number | null {
|
|
643
|
+
if (!value) return null;
|
|
644
|
+
const remainingPercent = readNumber(value[options.remainingPercent ?? "remaining_percent"]);
|
|
645
|
+
if (remainingPercent !== null) return Math.max(0, Math.min(100, 100 - remainingPercent));
|
|
646
|
+
|
|
647
|
+
const total = readNumber(value[options.total ?? "limit"]);
|
|
648
|
+
const used = readNumber(value[options.used ?? "used"]);
|
|
649
|
+
const remaining = readNumber(value[options.remaining ?? "remaining"]);
|
|
650
|
+
if (total === null || total <= 0) return null;
|
|
651
|
+
if (used !== null) return Math.max(0, Math.min(100, used / total * 100));
|
|
652
|
+
if (remaining !== null) return Math.max(0, Math.min(100, (total - remaining) / total * 100));
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function normalizeIsoDate(value: unknown): string | undefined {
|
|
657
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
658
|
+
const normalized = value.trim().replace(/(\.\d{3})\d+(?=Z|[+-]\d\d:\d\d$)/, "$1");
|
|
659
|
+
return Number.isFinite(new Date(normalized).getTime()) ? normalized : undefined;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function isoFromEpoch(value: unknown): string | undefined {
|
|
663
|
+
const raw = readNumber(value);
|
|
664
|
+
if (raw === null || raw <= 0) return undefined;
|
|
665
|
+
const milliseconds = raw > 1_000_000_000_000 ? raw : raw * 1000;
|
|
666
|
+
const date = new Date(milliseconds);
|
|
667
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : undefined;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function resetFromRemains(value: unknown, nowMs: number): string | undefined {
|
|
671
|
+
const raw = readNumber(value);
|
|
672
|
+
if (raw === null || raw <= 0) return undefined;
|
|
673
|
+
const milliseconds = raw > 1_000_000 ? raw : raw * 1000;
|
|
674
|
+
return new Date(nowMs + milliseconds).toISOString();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export function extractKimiUsageFromPayload(payload: unknown, nowMs = Date.now()): UsageData | null {
|
|
678
|
+
const root = asObject(payload);
|
|
679
|
+
if (!root) return null;
|
|
680
|
+
const webUsages = Array.isArray(root.usages) ? root.usages : undefined;
|
|
681
|
+
const codingUsage = webUsages?.map(asObject).find((entry) =>
|
|
682
|
+
String(entry?.scope ?? "").toUpperCase() === "FEATURE_CODING") ?? root;
|
|
683
|
+
const dataRows = Array.isArray(codingUsage.data) ? codingUsage.data.map(asObject).filter(Boolean) : [];
|
|
684
|
+
const usage = asObject(codingUsage.usage) ?? asObject(codingUsage.detail) ??
|
|
685
|
+
dataRows.find((entry) => String(entry?.model_name ?? entry?.modelName ?? "").toLowerCase() === "all");
|
|
686
|
+
const limits = Array.isArray(codingUsage.limits)
|
|
687
|
+
? codingUsage.limits
|
|
688
|
+
: dataRows.filter((entry) => entry !== usage);
|
|
689
|
+
const sessionLimit = limits.map(asObject).find((entry) => {
|
|
690
|
+
const window = asObject(entry?.window);
|
|
691
|
+
const duration = readNumber(window?.duration);
|
|
692
|
+
const unit = String(window?.timeUnit ?? window?.time_unit ?? "").toUpperCase();
|
|
693
|
+
return duration === 300 && unit.includes("MINUTE");
|
|
694
|
+
}) ?? limits.map(asObject).find((entry) => entry !== null);
|
|
695
|
+
const sessionDetail = asObject(sessionLimit?.detail) ?? sessionLimit;
|
|
696
|
+
|
|
697
|
+
const session = usedPercentFromCounts(sessionDetail);
|
|
698
|
+
const weekly = usedPercentFromCounts(usage);
|
|
699
|
+
if (session === null || weekly === null) return null;
|
|
700
|
+
|
|
701
|
+
const sessionReset = normalizeIsoDate(sessionDetail?.resetTime ?? sessionDetail?.reset_at ?? sessionDetail?.reset_time);
|
|
702
|
+
const weeklyReset = normalizeIsoDate(usage?.resetTime ?? usage?.reset_at ?? usage?.reset_time);
|
|
703
|
+
return hydrateUsageResets({
|
|
704
|
+
...normalizeUsagePair(session, weekly),
|
|
705
|
+
sessionLabel: "5-hour",
|
|
706
|
+
weeklyLabel: "Weekly",
|
|
707
|
+
sessionResetsAt: sessionReset,
|
|
708
|
+
weeklyResetsAt: weeklyReset,
|
|
709
|
+
}, nowMs);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export async function fetchKimiUsage(token: string, config: FetchConfig = {}): Promise<UsageData> {
|
|
713
|
+
const endpoints = config.endpoints ?? resolveUsageEndpoints(config.env);
|
|
714
|
+
const result = await requestJson(endpoints.kimi, {
|
|
715
|
+
headers: {
|
|
716
|
+
Authorization: `Bearer ${token}`,
|
|
717
|
+
"User-Agent": "KimiCLI/1.5",
|
|
718
|
+
},
|
|
719
|
+
}, config);
|
|
720
|
+
if (!result.ok) return { session: 0, weekly: 0, error: result.error };
|
|
721
|
+
return extractKimiUsageFromPayload(result.data) ?? {
|
|
722
|
+
session: 0,
|
|
723
|
+
weekly: 0,
|
|
724
|
+
error: "unrecognized response shape",
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
interface MiniMaxWindow {
|
|
729
|
+
percent: number;
|
|
730
|
+
resetsAt?: string;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function pickHighestWindow(windows: MiniMaxWindow[]): MiniMaxWindow | undefined {
|
|
734
|
+
return windows.reduce<MiniMaxWindow | undefined>((highest, window) =>
|
|
735
|
+
!highest || window.percent > highest.percent ? window : highest, undefined);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function miniMaxResetAt(value: Record<string, unknown>, prefix: "current" | "weekly", nowMs: number): string | undefined {
|
|
739
|
+
const end = prefix === "current"
|
|
740
|
+
? value.end_time ?? value.endTime
|
|
741
|
+
: value.weekly_end_time ?? value.weeklyEndTime;
|
|
742
|
+
const remains = prefix === "current"
|
|
743
|
+
? value.remains_time ?? value.remainsTime
|
|
744
|
+
: value.weekly_remains_time ?? value.weeklyRemainsTime;
|
|
745
|
+
const resetsAt = prefix === "current"
|
|
746
|
+
? value.current_resets_at ?? value.currentResetsAt
|
|
747
|
+
: value.weekly_resets_at ?? value.weeklyResetsAt;
|
|
748
|
+
return normalizeIsoDate(resetsAt) ?? isoFromEpoch(end) ?? resetFromRemains(remains, nowMs);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function extractMiniMaxCreditBalance(payload: unknown): AccountBalance | undefined {
|
|
752
|
+
const root = asObject(payload);
|
|
753
|
+
const data = asObject(root?.data) ?? root;
|
|
754
|
+
if (!data) return undefined;
|
|
755
|
+
const amount = readNumber(
|
|
756
|
+
data.points_balance ?? data.pointsBalance ??
|
|
757
|
+
data.point_balance ?? data.pointBalance ??
|
|
758
|
+
data.credits_balance ?? data.creditsBalance ??
|
|
759
|
+
data.credit_balance ?? data.creditBalance,
|
|
760
|
+
);
|
|
761
|
+
return amount === null ? undefined : { amount, unit: "credits", label: "Credit balance" };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
export function extractMiniMaxUsageFromPayload(payload: unknown, nowMs = Date.now()): UsageData | null {
|
|
765
|
+
const root = asObject(payload);
|
|
766
|
+
const data = asObject(root?.data) ?? root;
|
|
767
|
+
if (!data) return null;
|
|
768
|
+
const accountBalance = extractMiniMaxCreditBalance(payload);
|
|
769
|
+
|
|
770
|
+
const intervalWindows: MiniMaxWindow[] = [];
|
|
771
|
+
const weeklyWindows: MiniMaxWindow[] = [];
|
|
772
|
+
if (Array.isArray(data.services)) {
|
|
773
|
+
for (const rawService of data.services) {
|
|
774
|
+
const service = asObject(rawService);
|
|
775
|
+
if (!service) continue;
|
|
776
|
+
const directPercent = readPercentCandidate(readNumber(service.percent));
|
|
777
|
+
const percent = directPercent ?? usedPercentFromCounts(service, { total: "limit", used: "usage" });
|
|
778
|
+
if (percent === null) continue;
|
|
779
|
+
const windowType = String(service.window_type ?? service.windowType ?? "").toLowerCase();
|
|
780
|
+
const resetsAt = normalizeIsoDate(service.resets_at ?? service.reset_time ?? service.end_time);
|
|
781
|
+
(windowType.includes("week") ? weeklyWindows : intervalWindows).push({ percent, resetsAt });
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (Array.isArray(data.model_remains ?? data.modelRemains)) {
|
|
786
|
+
for (const rawModel of (data.model_remains ?? data.modelRemains) as unknown[]) {
|
|
787
|
+
const raw = asObject(rawModel);
|
|
788
|
+
if (!raw) continue;
|
|
789
|
+
const model: Record<string, unknown> = {
|
|
790
|
+
...raw,
|
|
791
|
+
current_interval_remaining_percent:
|
|
792
|
+
raw.current_interval_remaining_percent ?? raw.currentIntervalRemainingPercent,
|
|
793
|
+
current_interval_total_count: raw.current_interval_total_count ?? raw.currentIntervalTotalCount,
|
|
794
|
+
current_interval_usage_count: raw.current_interval_usage_count ?? raw.currentIntervalUsageCount,
|
|
795
|
+
current_interval_status: raw.current_interval_status ?? raw.currentIntervalStatus,
|
|
796
|
+
current_weekly_remaining_percent:
|
|
797
|
+
raw.current_weekly_remaining_percent ?? raw.currentWeeklyRemainingPercent,
|
|
798
|
+
current_weekly_total_count: raw.current_weekly_total_count ?? raw.currentWeeklyTotalCount,
|
|
799
|
+
current_weekly_usage_count: raw.current_weekly_usage_count ?? raw.currentWeeklyUsageCount,
|
|
800
|
+
current_weekly_status: raw.current_weekly_status ?? raw.currentWeeklyStatus,
|
|
801
|
+
};
|
|
802
|
+
const unavailable = (prefix: "interval" | "weekly") =>
|
|
803
|
+
readNumber(model[`current_${prefix}_status`]) === 3 &&
|
|
804
|
+
(readNumber(model[`current_${prefix}_remaining_percent`]) ?? 0) >= 100 &&
|
|
805
|
+
(readNumber(model[`current_${prefix}_total_count`]) ?? 0) === 0 &&
|
|
806
|
+
(readNumber(model[`current_${prefix}_usage_count`]) ?? 0) === 0;
|
|
807
|
+
const interval = unavailable("interval") ? null : usedPercentFromCounts(model, {
|
|
808
|
+
remainingPercent: "current_interval_remaining_percent",
|
|
809
|
+
total: "current_interval_total_count",
|
|
810
|
+
remaining: "current_interval_usage_count",
|
|
811
|
+
});
|
|
812
|
+
if (interval !== null) {
|
|
813
|
+
intervalWindows.push({ percent: interval, resetsAt: miniMaxResetAt(model, "current", nowMs) });
|
|
814
|
+
}
|
|
815
|
+
const weekly = unavailable("weekly") ? null : usedPercentFromCounts(model, {
|
|
816
|
+
remainingPercent: "current_weekly_remaining_percent",
|
|
817
|
+
total: "current_weekly_total_count",
|
|
818
|
+
remaining: "current_weekly_usage_count",
|
|
819
|
+
});
|
|
820
|
+
if (weekly !== null) {
|
|
821
|
+
weeklyWindows.push({ percent: weekly, resetsAt: miniMaxResetAt(model, "weekly", nowMs) });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const session = pickHighestWindow(intervalWindows);
|
|
827
|
+
const weekly = pickHighestWindow(weeklyWindows);
|
|
828
|
+
if (!session) {
|
|
829
|
+
return accountBalance
|
|
830
|
+
? { session: 0, weekly: 0, quotaHidden: true, accountBalance }
|
|
831
|
+
: null;
|
|
832
|
+
}
|
|
833
|
+
return hydrateUsageResets({
|
|
834
|
+
session: Number(session.percent.toFixed(2)),
|
|
835
|
+
accountBalance,
|
|
836
|
+
weekly: Number((weekly?.percent ?? 0).toFixed(2)),
|
|
837
|
+
sessionLabel: "Interval",
|
|
838
|
+
weeklyLabel: "Weekly",
|
|
839
|
+
weeklyHidden: !weekly,
|
|
840
|
+
sessionResetsAt: session.resetsAt,
|
|
841
|
+
weeklyResetsAt: weekly?.resetsAt,
|
|
842
|
+
}, nowMs);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function miniMaxPayloadStatus(payload: unknown): number | null {
|
|
846
|
+
const root = asObject(payload);
|
|
847
|
+
const data = asObject(root?.data);
|
|
848
|
+
const baseResponse = asObject(data?.base_resp ?? data?.baseResp ?? root?.base_resp ?? root?.baseResp);
|
|
849
|
+
return readNumber(baseResponse?.status_code ?? baseResponse?.statusCode);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function miniMaxPayloadError(payload: unknown): string | null {
|
|
853
|
+
const root = asObject(payload);
|
|
854
|
+
const data = asObject(root?.data);
|
|
855
|
+
const baseResponse = asObject(data?.base_resp ?? data?.baseResp ?? root?.base_resp ?? root?.baseResp);
|
|
856
|
+
const status = miniMaxPayloadStatus(payload);
|
|
857
|
+
if (status === null || status === 0) return null;
|
|
858
|
+
const message = baseResponse?.status_msg ?? baseResponse?.statusMessage;
|
|
859
|
+
return typeof message === "string" && message.trim()
|
|
860
|
+
? `API ${status}: ${message.trim()}`
|
|
861
|
+
: `API ${status}`;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
export async function fetchMiniMaxUsage(
|
|
865
|
+
token: string,
|
|
866
|
+
provider: "minimax" | "minimax-cn" = "minimax",
|
|
867
|
+
config: FetchConfig = {},
|
|
868
|
+
): Promise<UsageData> {
|
|
869
|
+
const endpoints = config.endpoints ?? resolveUsageEndpoints(config.env);
|
|
870
|
+
const candidates = provider === "minimax-cn"
|
|
871
|
+
? [endpoints.minimaxCn, endpoints.minimaxCnLegacy]
|
|
872
|
+
: [endpoints.minimax, endpoints.minimaxLegacy];
|
|
873
|
+
let lastError = "usage request failed";
|
|
874
|
+
let credentialError: string | undefined;
|
|
875
|
+
let noActiveTokenPlan = false;
|
|
876
|
+
|
|
877
|
+
for (const endpoint of [...new Set(candidates)]) {
|
|
878
|
+
const result = await requestJson(endpoint, {
|
|
879
|
+
headers: {
|
|
880
|
+
Authorization: `Bearer ${token}`,
|
|
881
|
+
Accept: "application/json",
|
|
882
|
+
},
|
|
883
|
+
}, config);
|
|
884
|
+
if (!result.ok) {
|
|
885
|
+
lastError = result.error;
|
|
886
|
+
if (result.status === 401 || result.status === 403) credentialError ??= result.error;
|
|
887
|
+
if (config.signal?.aborted) break;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const payloadStatus = miniMaxPayloadStatus(result.data);
|
|
891
|
+
const payloadError = miniMaxPayloadError(result.data);
|
|
892
|
+
const usage = extractMiniMaxUsageFromPayload(result.data);
|
|
893
|
+
if (usage && (!payloadError || usage.quotaHidden)) return usage;
|
|
894
|
+
if (payloadStatus === 2062) {
|
|
895
|
+
noActiveTokenPlan = true;
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
if (payloadError) {
|
|
899
|
+
lastError = payloadError;
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
lastError = "unrecognized response shape";
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (noActiveTokenPlan) {
|
|
906
|
+
return {
|
|
907
|
+
session: 0,
|
|
908
|
+
weekly: 0,
|
|
909
|
+
quotaHidden: true,
|
|
910
|
+
notice: "No active Token Plan · check Credit balance in the MiniMax console",
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
return { session: 0, weekly: 0, error: credentialError ?? lastError };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
export function extractOpenRouterUsageFromPayloads(
|
|
917
|
+
creditsPayload: unknown,
|
|
918
|
+
keyPayload: unknown,
|
|
919
|
+
): UsageData | null {
|
|
920
|
+
const credits = asObject(asObject(creditsPayload)?.data) ?? asObject(creditsPayload);
|
|
921
|
+
const key = asObject(asObject(keyPayload)?.data) ?? asObject(keyPayload);
|
|
922
|
+
|
|
923
|
+
const totalCredits = readNumber(credits?.total_credits ?? credits?.totalCredits);
|
|
924
|
+
const totalUsage = readNumber(credits?.total_usage ?? credits?.totalUsage);
|
|
925
|
+
const accountBalance = totalCredits !== null && totalUsage !== null
|
|
926
|
+
? {
|
|
927
|
+
amount: Number((totalCredits - totalUsage).toFixed(6)),
|
|
928
|
+
unit: "USD",
|
|
929
|
+
label: "Balance",
|
|
930
|
+
}
|
|
931
|
+
: undefined;
|
|
932
|
+
|
|
933
|
+
const spendValues = {
|
|
934
|
+
daily: readNumber(key?.usage_daily ?? key?.usageDaily),
|
|
935
|
+
weekly: readNumber(key?.usage_weekly ?? key?.usageWeekly),
|
|
936
|
+
monthly: readNumber(key?.usage_monthly ?? key?.usageMonthly),
|
|
937
|
+
lifetime: readNumber(key?.usage),
|
|
938
|
+
};
|
|
939
|
+
const accountSpend = Object.values(spendValues).some((value) => value !== null)
|
|
940
|
+
? {
|
|
941
|
+
unit: "USD",
|
|
942
|
+
daily: spendValues.daily ?? undefined,
|
|
943
|
+
weekly: spendValues.weekly ?? undefined,
|
|
944
|
+
monthly: spendValues.monthly ?? undefined,
|
|
945
|
+
lifetime: spendValues.lifetime ?? undefined,
|
|
946
|
+
}
|
|
947
|
+
: undefined;
|
|
948
|
+
|
|
949
|
+
const limit = readNumber(key?.limit);
|
|
950
|
+
const remaining = readNumber(key?.limit_remaining ?? key?.limitRemaining);
|
|
951
|
+
const limitUsed = limit !== null && limit > 0 && remaining !== null
|
|
952
|
+
? Math.max(0, Math.min(limit, limit - remaining))
|
|
953
|
+
: null;
|
|
954
|
+
const limitPercent = limitUsed !== null && limit !== null ? limitUsed / limit * 100 : null;
|
|
955
|
+
if (!accountBalance && !accountSpend && limitPercent === null) return null;
|
|
956
|
+
|
|
957
|
+
return {
|
|
958
|
+
session: limitPercent === null ? 0 : Number(limitPercent.toFixed(2)),
|
|
959
|
+
weekly: 0,
|
|
960
|
+
quotaHidden: limitPercent === null,
|
|
961
|
+
weeklyHidden: true,
|
|
962
|
+
sessionLabel: "Key limit",
|
|
963
|
+
accountBalance,
|
|
964
|
+
accountSpend,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
export async function fetchOpenRouterUsage(token: string, config: FetchConfig = {}): Promise<UsageData> {
|
|
969
|
+
const endpoints = config.endpoints ?? resolveUsageEndpoints(config.env);
|
|
970
|
+
const headers = { Authorization: `Bearer ${token}`, Accept: "application/json" };
|
|
971
|
+
const [creditsResult, keyResult] = await Promise.all([
|
|
972
|
+
requestJson(endpoints.openRouterCredits, { headers }, config),
|
|
973
|
+
requestJson(endpoints.openRouterKey, { headers }, config),
|
|
974
|
+
]);
|
|
975
|
+
const usage = extractOpenRouterUsageFromPayloads(
|
|
976
|
+
creditsResult.ok ? creditsResult.data : undefined,
|
|
977
|
+
keyResult.ok ? keyResult.data : undefined,
|
|
978
|
+
);
|
|
979
|
+
if (usage) return usage;
|
|
980
|
+
|
|
981
|
+
const errors = [
|
|
982
|
+
creditsResult.ok ? undefined : `credits: ${creditsResult.error}`,
|
|
983
|
+
keyResult.ok ? undefined : `key: ${keyResult.error}`,
|
|
984
|
+
].filter((value): value is string => Boolean(value));
|
|
985
|
+
return {
|
|
986
|
+
session: 0,
|
|
987
|
+
weekly: 0,
|
|
988
|
+
error: errors.length > 0 ? errors.join("; ") : "unrecognized response shape",
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
export function extractDeepSeekBalanceFromPayload(payload: unknown): UsageData | null {
|
|
993
|
+
const root = asObject(payload);
|
|
994
|
+
const rawBalances = Array.isArray(root?.balance_infos) ? root.balance_infos : [];
|
|
995
|
+
const balances = rawBalances.map(asObject).filter((value): value is Record<string, unknown> => value !== null);
|
|
996
|
+
if (balances.length === 0) return null;
|
|
997
|
+
|
|
998
|
+
const parsed = balances.flatMap((balance) => {
|
|
999
|
+
const unit = typeof balance.currency === "string" ? balance.currency.toUpperCase() : "USD";
|
|
1000
|
+
const total = readNumber(balance.total_balance ?? balance.totalBalance);
|
|
1001
|
+
if (total === null) return [];
|
|
1002
|
+
return [{
|
|
1003
|
+
total: { amount: total, unit, label: "Total balance" } satisfies AccountBalance,
|
|
1004
|
+
toppedUp: readNumber(balance.topped_up_balance ?? balance.toppedUpBalance),
|
|
1005
|
+
granted: readNumber(balance.granted_balance ?? balance.grantedBalance),
|
|
1006
|
+
}];
|
|
1007
|
+
});
|
|
1008
|
+
const primary = parsed[0];
|
|
1009
|
+
if (!primary) return null;
|
|
1010
|
+
|
|
1011
|
+
const details: AccountBalance[] = [];
|
|
1012
|
+
if (primary.toppedUp !== null) {
|
|
1013
|
+
details.push({ amount: primary.toppedUp, unit: primary.total.unit, label: "Topped up" });
|
|
1014
|
+
}
|
|
1015
|
+
if (primary.granted !== null) {
|
|
1016
|
+
details.push({ amount: primary.granted, unit: primary.total.unit, label: "Granted" });
|
|
1017
|
+
}
|
|
1018
|
+
for (const additional of parsed.slice(1)) details.push(additional.total);
|
|
1019
|
+
|
|
1020
|
+
return {
|
|
1021
|
+
session: 0,
|
|
1022
|
+
weekly: 0,
|
|
1023
|
+
quotaHidden: true,
|
|
1024
|
+
accountBalance: primary.total,
|
|
1025
|
+
accountBalanceDetails: details,
|
|
1026
|
+
warning: root?.is_available === false ? "Balance is not currently available for API use" : undefined,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
export async function fetchDeepSeekBalance(token: string, config: FetchConfig = {}): Promise<UsageData> {
|
|
1031
|
+
const endpoints = config.endpoints ?? resolveUsageEndpoints(config.env);
|
|
1032
|
+
const result = await requestJson(endpoints.deepSeekBalance, {
|
|
1033
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
1034
|
+
}, config);
|
|
1035
|
+
if (!result.ok) return { session: 0, weekly: 0, error: result.error };
|
|
1036
|
+
return extractDeepSeekBalanceFromPayload(result.data) ?? {
|
|
1037
|
+
session: 0,
|
|
1038
|
+
weekly: 0,
|
|
1039
|
+
error: "unrecognized response shape",
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
export function extractMoonshotBalanceFromPayload(
|
|
1044
|
+
payload: unknown,
|
|
1045
|
+
provider: "moonshot" | "moonshot-cn" = "moonshot",
|
|
1046
|
+
): UsageData | null {
|
|
1047
|
+
const root = asObject(payload);
|
|
1048
|
+
const data = asObject(root?.data) ?? root;
|
|
1049
|
+
if (!data) return null;
|
|
1050
|
+
const available = readNumber(data.available_balance ?? data.availableBalance);
|
|
1051
|
+
if (available === null) return null;
|
|
1052
|
+
const cash = readNumber(data.cash_balance ?? data.cashBalance);
|
|
1053
|
+
const voucher = readNumber(data.voucher_balance ?? data.voucherBalance);
|
|
1054
|
+
const unit = provider === "moonshot-cn" ? "CNY" : "USD";
|
|
1055
|
+
const details: AccountBalance[] = [];
|
|
1056
|
+
if (cash !== null) details.push({ amount: cash, unit, label: "Cash" });
|
|
1057
|
+
if (voucher !== null) details.push({ amount: voucher, unit, label: "Voucher" });
|
|
1058
|
+
|
|
1059
|
+
return {
|
|
1060
|
+
session: 0,
|
|
1061
|
+
weekly: 0,
|
|
1062
|
+
quotaHidden: true,
|
|
1063
|
+
accountBalance: { amount: available, unit, label: "Available balance" },
|
|
1064
|
+
accountBalanceDetails: details,
|
|
1065
|
+
warning: available <= 0 ? "Balance exhausted; inference requests may be rejected" : undefined,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
export async function fetchMoonshotBalance(
|
|
1070
|
+
token: string,
|
|
1071
|
+
provider: "moonshot" | "moonshot-cn" = "moonshot",
|
|
1072
|
+
config: FetchConfig = {},
|
|
1073
|
+
): Promise<UsageData> {
|
|
1074
|
+
const endpoints = config.endpoints ?? resolveUsageEndpoints(config.env);
|
|
1075
|
+
const endpoint = provider === "moonshot-cn" ? endpoints.moonshotCnBalance : endpoints.moonshotBalance;
|
|
1076
|
+
const result = await requestJson(endpoint, {
|
|
1077
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
1078
|
+
}, config);
|
|
1079
|
+
if (!result.ok) return { session: 0, weekly: 0, error: result.error };
|
|
1080
|
+
return extractMoonshotBalanceFromPayload(result.data, provider) ?? {
|
|
1081
|
+
session: 0,
|
|
1082
|
+
weekly: 0,
|
|
1083
|
+
error: "unrecognized response shape",
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
548
1087
|
/** Parse ZAI limits where unit 3 is the five-hour window and unit 6 the weekly window. */
|
|
549
1088
|
export function extractZaiUsageFromPayload(payload: unknown, nowMs = Date.now()): UsageData | null {
|
|
550
1089
|
const data = payload as any;
|
|
@@ -597,6 +1136,13 @@ export function detectProvider(
|
|
|
597
1136
|
case "anthropic": return "claude";
|
|
598
1137
|
case "zai": return "zai";
|
|
599
1138
|
case "zai-coding-cn": return "zai-cn";
|
|
1139
|
+
case "kimi-coding": return "kimi";
|
|
1140
|
+
case "minimax": return "minimax";
|
|
1141
|
+
case "minimax-cn": return "minimax-cn";
|
|
1142
|
+
case "openrouter": return "openrouter";
|
|
1143
|
+
case "deepseek": return "deepseek";
|
|
1144
|
+
case "moonshotai": return "moonshot";
|
|
1145
|
+
case "moonshotai-cn": return "moonshot-cn";
|
|
600
1146
|
default: return null;
|
|
601
1147
|
}
|
|
602
1148
|
}
|
|
@@ -607,6 +1153,13 @@ export function providerToPiProviderId(provider: ProviderKey): PiProviderId {
|
|
|
607
1153
|
case "claude": return "anthropic";
|
|
608
1154
|
case "zai": return "zai";
|
|
609
1155
|
case "zai-cn": return "zai-coding-cn";
|
|
1156
|
+
case "kimi": return "kimi-coding";
|
|
1157
|
+
case "minimax": return "minimax";
|
|
1158
|
+
case "minimax-cn": return "minimax-cn";
|
|
1159
|
+
case "openrouter": return "openrouter";
|
|
1160
|
+
case "deepseek": return "deepseek";
|
|
1161
|
+
case "moonshot": return "moonshotai";
|
|
1162
|
+
case "moonshot-cn": return "moonshotai-cn";
|
|
610
1163
|
}
|
|
611
1164
|
}
|
|
612
1165
|
|
|
@@ -626,7 +1179,19 @@ export async function fetchAllUsages(
|
|
|
626
1179
|
config: FetchAllUsagesConfig = {},
|
|
627
1180
|
): Promise<UsageByProvider> {
|
|
628
1181
|
const endpoints = config.endpoints ?? resolveUsageEndpoints(config.env);
|
|
629
|
-
const results: UsageByProvider = {
|
|
1182
|
+
const results: UsageByProvider = {
|
|
1183
|
+
codex: null,
|
|
1184
|
+
claude: null,
|
|
1185
|
+
zai: null,
|
|
1186
|
+
"zai-cn": null,
|
|
1187
|
+
kimi: null,
|
|
1188
|
+
minimax: null,
|
|
1189
|
+
"minimax-cn": null,
|
|
1190
|
+
openrouter: null,
|
|
1191
|
+
deepseek: null,
|
|
1192
|
+
moonshot: null,
|
|
1193
|
+
"moonshot-cn": null,
|
|
1194
|
+
};
|
|
630
1195
|
const tasks: Promise<void>[] = [];
|
|
631
1196
|
|
|
632
1197
|
const assign = (provider: ProviderKey, request: Promise<UsageData>) => {
|
|
@@ -647,7 +1212,29 @@ export async function fetchAllUsages(
|
|
|
647
1212
|
}
|
|
648
1213
|
if (tokens.zai) assign("zai", fetchZaiUsage(tokens.zai, "zai", { ...config, endpoints }));
|
|
649
1214
|
if (tokens["zai-cn"]) assign("zai-cn", fetchZaiUsage(tokens["zai-cn"], "zai-cn", { ...config, endpoints }));
|
|
1215
|
+
if (tokens.kimi) assign("kimi", fetchKimiUsage(tokens.kimi, { ...config, endpoints }));
|
|
1216
|
+
if (tokens.minimax) assign("minimax", fetchMiniMaxUsage(tokens.minimax, "minimax", { ...config, endpoints }));
|
|
1217
|
+
if (tokens["minimax-cn"]) {
|
|
1218
|
+
assign("minimax-cn", fetchMiniMaxUsage(tokens["minimax-cn"], "minimax-cn", { ...config, endpoints }));
|
|
1219
|
+
}
|
|
1220
|
+
if (tokens.openrouter) assign("openrouter", fetchOpenRouterUsage(tokens.openrouter, { ...config, endpoints }));
|
|
1221
|
+
if (tokens.deepseek) assign("deepseek", fetchDeepSeekBalance(tokens.deepseek, { ...config, endpoints }));
|
|
1222
|
+
if (tokens.moonshot) assign("moonshot", fetchMoonshotBalance(tokens.moonshot, "moonshot", { ...config, endpoints }));
|
|
1223
|
+
if (tokens["moonshot-cn"]) {
|
|
1224
|
+
assign("moonshot-cn", fetchMoonshotBalance(tokens["moonshot-cn"], "moonshot-cn", { ...config, endpoints }));
|
|
1225
|
+
}
|
|
650
1226
|
|
|
651
1227
|
await Promise.all(tasks);
|
|
1228
|
+
|
|
1229
|
+
// Pi intentionally uses MOONSHOT_API_KEY for both regional providers. When one
|
|
1230
|
+
// key works in only one region, hide the expected regional auth failure from
|
|
1231
|
+
// the all-provider view while preserving active-provider polling behavior.
|
|
1232
|
+
if (tokens.moonshot && tokens.moonshot === tokens["moonshot-cn"]) {
|
|
1233
|
+
if (results.moonshot && !results.moonshot.error && results["moonshot-cn"]?.error) {
|
|
1234
|
+
results["moonshot-cn"] = null;
|
|
1235
|
+
} else if (results["moonshot-cn"] && !results["moonshot-cn"].error && results.moonshot?.error) {
|
|
1236
|
+
results.moonshot = null;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
652
1239
|
return results;
|
|
653
1240
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Quota, balance, and spend indicators for providers supported by current Pi releases. */
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
DynamicBorder,
|
|
@@ -22,9 +22,16 @@ import {
|
|
|
22
22
|
fetchAllUsages,
|
|
23
23
|
fetchClaudeUsageWithFallback,
|
|
24
24
|
fetchCodexUsage,
|
|
25
|
+
fetchDeepSeekBalance,
|
|
26
|
+
fetchKimiUsage,
|
|
27
|
+
fetchMiniMaxUsage,
|
|
28
|
+
fetchMoonshotBalance,
|
|
29
|
+
fetchOpenRouterUsage,
|
|
25
30
|
fetchZaiUsage,
|
|
26
31
|
providerToPiProviderId,
|
|
27
32
|
resolveUsageEndpoints,
|
|
33
|
+
type AccountBalance,
|
|
34
|
+
type AccountSpend,
|
|
28
35
|
type ProviderKey,
|
|
29
36
|
type UsageByProvider,
|
|
30
37
|
type UsageData,
|
|
@@ -33,15 +40,63 @@ import {
|
|
|
33
40
|
|
|
34
41
|
const POLL_INTERVAL_MS = 2 * 60 * 1000;
|
|
35
42
|
const STATUS_KEY = "usage-bars";
|
|
36
|
-
const PROVIDERS: readonly ProviderKey[] = [
|
|
43
|
+
const PROVIDERS: readonly ProviderKey[] = [
|
|
44
|
+
"codex",
|
|
45
|
+
"claude",
|
|
46
|
+
"zai",
|
|
47
|
+
"zai-cn",
|
|
48
|
+
"kimi",
|
|
49
|
+
"minimax",
|
|
50
|
+
"minimax-cn",
|
|
51
|
+
"openrouter",
|
|
52
|
+
"deepseek",
|
|
53
|
+
"moonshot",
|
|
54
|
+
"moonshot-cn",
|
|
55
|
+
];
|
|
37
56
|
|
|
38
57
|
const PROVIDER_LABELS: Record<ProviderKey, string> = {
|
|
39
58
|
codex: "Codex",
|
|
40
59
|
claude: "Claude",
|
|
41
60
|
zai: "ZAI Coding Plan (Global)",
|
|
42
61
|
"zai-cn": "ZAI Coding Plan (China)",
|
|
62
|
+
kimi: "Kimi For Coding",
|
|
63
|
+
minimax: "MiniMax Coding Plan (Global)",
|
|
64
|
+
"minimax-cn": "MiniMax Coding Plan (China)",
|
|
65
|
+
openrouter: "OpenRouter",
|
|
66
|
+
deepseek: "DeepSeek",
|
|
67
|
+
moonshot: "Moonshot/Kimi API (Global)",
|
|
68
|
+
"moonshot-cn": "Moonshot/Kimi API (China)",
|
|
43
69
|
};
|
|
44
70
|
|
|
71
|
+
function formatFinancialAmount(amount: number, unit: string): string {
|
|
72
|
+
if (/^[A-Z]{3}$/.test(unit)) {
|
|
73
|
+
return new Intl.NumberFormat("en-US", {
|
|
74
|
+
style: "currency",
|
|
75
|
+
currency: unit,
|
|
76
|
+
minimumFractionDigits: 2,
|
|
77
|
+
maximumFractionDigits: 5,
|
|
78
|
+
}).format(amount);
|
|
79
|
+
}
|
|
80
|
+
const formatted = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(amount);
|
|
81
|
+
return `${formatted} ${unit}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatAccountBalance(balance: AccountBalance): string {
|
|
85
|
+
return `${balance.label} · ${formatFinancialAmount(balance.amount, balance.unit)}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatAccountSpend(spend: AccountSpend): string {
|
|
89
|
+
const values = [
|
|
90
|
+
spend.daily === undefined ? undefined : `today ${formatFinancialAmount(spend.daily, spend.unit)}`,
|
|
91
|
+
spend.weekly === undefined ? undefined : `week ${formatFinancialAmount(spend.weekly, spend.unit)}`,
|
|
92
|
+
spend.monthly === undefined ? undefined : `month ${formatFinancialAmount(spend.monthly, spend.unit)}`,
|
|
93
|
+
].filter((value): value is string => Boolean(value));
|
|
94
|
+
if (values.length === 0 && spend.lifetime !== undefined) {
|
|
95
|
+
values.push(`lifetime ${formatFinancialAmount(spend.lifetime, spend.unit)}`);
|
|
96
|
+
}
|
|
97
|
+
return `Spent · ${values.join(" · ")}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
45
100
|
interface SubscriptionItem {
|
|
46
101
|
name: string;
|
|
47
102
|
provider: ProviderKey;
|
|
@@ -135,12 +190,12 @@ class UsageSelectorComponent extends Container implements Focusable {
|
|
|
135
190
|
|
|
136
191
|
private updateHint(): void {
|
|
137
192
|
if (this.hint === "loading") {
|
|
138
|
-
this.hintText.setText(this.theme.fg("dim", "Fetching
|
|
193
|
+
this.hintText.setText(this.theme.fg("dim", "Fetching quota, balance, and spend from configured providers…"));
|
|
139
194
|
} else if (this.hint === "error") {
|
|
140
195
|
this.hintText.setText(this.theme.fg("error", "Failed to fetch usage data"));
|
|
141
196
|
} else {
|
|
142
197
|
this.hintText.setText(
|
|
143
|
-
this.theme.fg("muted", "Only showing configured
|
|
198
|
+
this.theme.fg("muted", "Only showing configured usage providers. ") +
|
|
144
199
|
this.theme.fg("dim", "✓ = active provider"),
|
|
145
200
|
);
|
|
146
201
|
}
|
|
@@ -197,19 +252,49 @@ class UsageSelectorComponent extends Container implements Focusable {
|
|
|
197
252
|
const weeklyReset = item.data.weeklyResetsIn
|
|
198
253
|
? theme.fg("dim", ` resets in ${item.data.weeklyResetsIn}`)
|
|
199
254
|
: "";
|
|
255
|
+
const sessionLabel = (item.data.sessionLabel ?? "Session").slice(0, 9).padEnd(10);
|
|
256
|
+
const weeklyLabel = (item.data.weeklyLabel ?? "Weekly").slice(0, 9).padEnd(10);
|
|
200
257
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
theme.fg(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
258
|
+
if (!item.data.quotaHidden) {
|
|
259
|
+
this.listContainer.addChild(new Text(
|
|
260
|
+
indent + theme.fg("muted", sessionLabel) + this.renderBar(session) + " " +
|
|
261
|
+
theme.fg(colorForPercent(session), `${session}%`.padStart(4)) + sessionReset,
|
|
262
|
+
0,
|
|
263
|
+
0,
|
|
264
|
+
));
|
|
265
|
+
if (!item.data.weeklyHidden) {
|
|
266
|
+
this.listContainer.addChild(new Text(
|
|
267
|
+
indent + theme.fg("muted", weeklyLabel) + this.renderBar(weekly) + " " +
|
|
268
|
+
theme.fg(colorForPercent(weekly), `${weekly}%`.padStart(4)) + weeklyReset,
|
|
269
|
+
0,
|
|
270
|
+
0,
|
|
271
|
+
));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (item.data.accountBalance) {
|
|
275
|
+
this.listContainer.addChild(new Text(
|
|
276
|
+
indent + theme.fg("muted", formatAccountBalance(item.data.accountBalance)),
|
|
277
|
+
0,
|
|
278
|
+
0,
|
|
279
|
+
));
|
|
280
|
+
}
|
|
281
|
+
for (const balance of item.data.accountBalanceDetails ?? []) {
|
|
282
|
+
this.listContainer.addChild(new Text(
|
|
283
|
+
indent + theme.fg("dim", formatAccountBalance(balance)),
|
|
284
|
+
0,
|
|
285
|
+
0,
|
|
286
|
+
));
|
|
287
|
+
}
|
|
288
|
+
if (item.data.accountSpend) {
|
|
289
|
+
this.listContainer.addChild(new Text(
|
|
290
|
+
indent + theme.fg("muted", formatAccountSpend(item.data.accountSpend)),
|
|
291
|
+
0,
|
|
292
|
+
0,
|
|
293
|
+
));
|
|
294
|
+
}
|
|
295
|
+
if (item.data.notice) {
|
|
296
|
+
this.listContainer.addChild(new Text(indent + theme.fg("muted", item.data.notice), 0, 0));
|
|
297
|
+
}
|
|
213
298
|
|
|
214
299
|
if (typeof item.data.extraSpend === "number" && typeof item.data.extraLimit === "number") {
|
|
215
300
|
this.listContainer.addChild(new Text(
|
|
@@ -300,6 +385,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
300
385
|
claude: null,
|
|
301
386
|
zai: null,
|
|
302
387
|
"zai-cn": null,
|
|
388
|
+
kimi: null,
|
|
389
|
+
minimax: null,
|
|
390
|
+
"minimax-cn": null,
|
|
391
|
+
openrouter: null,
|
|
392
|
+
deepseek: null,
|
|
393
|
+
moonshot: null,
|
|
394
|
+
"moonshot-cn": null,
|
|
303
395
|
activeProvider: null,
|
|
304
396
|
available: {},
|
|
305
397
|
};
|
|
@@ -343,15 +435,43 @@ export default function (pi: ExtensionAPI): void {
|
|
|
343
435
|
ctx.ui.setStatus(STATUS_KEY, theme.fg("warning", `${label} usage unavailable (${data.error})`));
|
|
344
436
|
return;
|
|
345
437
|
}
|
|
438
|
+
if (data.quotaHidden) {
|
|
439
|
+
const financial = [
|
|
440
|
+
data.accountBalance ? formatAccountBalance(data.accountBalance) : undefined,
|
|
441
|
+
data.accountSpend?.monthly === undefined
|
|
442
|
+
? undefined
|
|
443
|
+
: `Month · ${formatFinancialAmount(data.accountSpend.monthly, data.accountSpend.unit)}`,
|
|
444
|
+
].filter((value): value is string => Boolean(value));
|
|
445
|
+
const summary = financial.length > 0 ? financial.join(" · ") : data.notice;
|
|
446
|
+
ctx.ui.setStatus(
|
|
447
|
+
STATUS_KEY,
|
|
448
|
+
summary ? theme.fg("dim", `${label} `) + theme.fg("muted", summary) : undefined,
|
|
449
|
+
);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
346
452
|
|
|
347
453
|
const session = clampPercent(data.session);
|
|
348
454
|
const weekly = clampPercent(data.weekly);
|
|
455
|
+
const sessionPrefix = data.sessionLabel === "5-hour"
|
|
456
|
+
? "5h "
|
|
457
|
+
: data.sessionLabel === "Interval"
|
|
458
|
+
? "I "
|
|
459
|
+
: data.sessionLabel === "Key limit"
|
|
460
|
+
? "L "
|
|
461
|
+
: "S ";
|
|
462
|
+
const weeklyStatus = data.weeklyHidden
|
|
463
|
+
? ""
|
|
464
|
+
: theme.fg("muted", " W ") + renderBar(theme, weekly) + " " + renderPercent(theme, weekly) +
|
|
465
|
+
(data.weeklyResetsIn ? theme.fg("dim", ` ⟳ ${data.weeklyResetsIn}`) : "");
|
|
349
466
|
const status =
|
|
350
467
|
theme.fg("dim", `${label} `) +
|
|
351
|
-
theme.fg("muted",
|
|
468
|
+
theme.fg("muted", sessionPrefix) + renderBar(theme, session) + " " + renderPercent(theme, session) +
|
|
352
469
|
(data.sessionResetsIn ? theme.fg("dim", ` ⟳ ${data.sessionResetsIn}`) : "") +
|
|
353
|
-
|
|
354
|
-
(data.
|
|
470
|
+
weeklyStatus +
|
|
471
|
+
(data.accountBalance ? theme.fg("muted", ` · ${formatAccountBalance(data.accountBalance)}`) : "") +
|
|
472
|
+
(data.accountSpend?.monthly === undefined
|
|
473
|
+
? ""
|
|
474
|
+
: theme.fg("muted", ` · Month ${formatFinancialAmount(data.accountSpend.monthly, data.accountSpend.unit)}`)) +
|
|
355
475
|
(data.stale ? theme.fg("warning", " stale") : "") +
|
|
356
476
|
(data.warning && !data.stale ? theme.fg("warning", " ⚠") : "");
|
|
357
477
|
ctx.ui.setStatus(STATUS_KEY, status);
|
|
@@ -404,6 +524,25 @@ export default function (pi: ExtensionAPI): void {
|
|
|
404
524
|
if (provider === "claude") state.claude = await fetchClaudeUsageWithFallback(credential.token, { signal });
|
|
405
525
|
if (provider === "zai") state.zai = await fetchZaiUsage(credential.token, "zai", { endpoints, signal });
|
|
406
526
|
if (provider === "zai-cn") state["zai-cn"] = await fetchZaiUsage(credential.token, "zai-cn", { endpoints, signal });
|
|
527
|
+
if (provider === "kimi") state.kimi = await fetchKimiUsage(credential.token, { endpoints, signal });
|
|
528
|
+
if (provider === "minimax") {
|
|
529
|
+
state.minimax = await fetchMiniMaxUsage(credential.token, "minimax", { endpoints, signal });
|
|
530
|
+
}
|
|
531
|
+
if (provider === "minimax-cn") {
|
|
532
|
+
state["minimax-cn"] = await fetchMiniMaxUsage(credential.token, "minimax-cn", { endpoints, signal });
|
|
533
|
+
}
|
|
534
|
+
if (provider === "openrouter") {
|
|
535
|
+
state.openrouter = await fetchOpenRouterUsage(credential.token, { endpoints, signal });
|
|
536
|
+
}
|
|
537
|
+
if (provider === "deepseek") {
|
|
538
|
+
state.deepseek = await fetchDeepSeekBalance(credential.token, { endpoints, signal });
|
|
539
|
+
}
|
|
540
|
+
if (provider === "moonshot") {
|
|
541
|
+
state.moonshot = await fetchMoonshotBalance(credential.token, "moonshot", { endpoints, signal });
|
|
542
|
+
}
|
|
543
|
+
if (provider === "moonshot-cn") {
|
|
544
|
+
state["moonshot-cn"] = await fetchMoonshotBalance(credential.token, "moonshot-cn", { endpoints, signal });
|
|
545
|
+
}
|
|
407
546
|
}
|
|
408
547
|
|
|
409
548
|
async function runPoll(): Promise<void> {
|
|
@@ -493,7 +632,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
493
632
|
});
|
|
494
633
|
|
|
495
634
|
pi.registerCommand("usage", {
|
|
496
|
-
description: "Show
|
|
635
|
+
description: "Show quota, balance, and spend for configured providers",
|
|
497
636
|
handler: async (_args, ctx) => {
|
|
498
637
|
currentContext = ctx;
|
|
499
638
|
updateProviderFrom(ctx.model);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hk_net/pi-usage-bars",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Quota, balance, and spend indicators for current Pi providers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi",
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
"usage",
|
|
10
10
|
"codex",
|
|
11
11
|
"anthropic",
|
|
12
|
-
"zai"
|
|
12
|
+
"zai",
|
|
13
|
+
"kimi",
|
|
14
|
+
"minimax",
|
|
15
|
+
"openrouter",
|
|
16
|
+
"deepseek",
|
|
17
|
+
"moonshot"
|
|
13
18
|
],
|
|
14
19
|
"license": "MIT",
|
|
15
20
|
"repository": {
|
|
@@ -27,6 +32,7 @@
|
|
|
27
32
|
"type": "module",
|
|
28
33
|
"files": [
|
|
29
34
|
"extensions",
|
|
35
|
+
"docs",
|
|
30
36
|
"README.md",
|
|
31
37
|
"CHANGELOG.md",
|
|
32
38
|
"LICENSE"
|