@wayner6/pi-usage 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -1,89 +1,97 @@
|
|
|
1
1
|
# Pi Usage
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[中文文档](./README_zh.md)
|
|
4
4
|
|
|
5
|
-
[
|
|
5
|
+
Pi Usage is a [Pi](https://github.com/earendil-works/pi-mono) and [pi-web](https://github.com/agegr/pi-web) plugin that shows the balance or quota of the provider behind the active model.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## What it shows
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- A compact status line for the active model, such as `Codex · 5h 92% · 7d 85%`.
|
|
10
|
+
- Remaining balance, rate-limit windows, and reset times when the provider exposes them.
|
|
11
|
+
- Details for every configured provider through `/usage`.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
| :--- | :--- | :--- |
|
|
13
|
-
| **OpenAI Codex** | **OAuth** (ChatGPT Plus / Pro) | Official 5-hour and 7-day sliding quota windows with reset countdowns |
|
|
14
|
-
| **xAI / Grok** | **OAuth** | Identity verification and active spending/credit limit status probe |
|
|
15
|
-
| **Anthropic Claude** | **OAuth** / **API Key** | Claude Pro/Max OAuth subscription detection or official API Key rate limits via headers |
|
|
16
|
-
| **DeepSeek Direct** | **API Key** | Exact account monetary balance (granted + topped-up in CNY/USD) |
|
|
17
|
-
| **GLM / 智谱 BigModel** | **API Key** | Coding Plan multi-tier sliding windows (5h, 7d, MCP); notice displayed if using standard Pay-as-you-go key (official API only supports quota display for Coding Plan) |
|
|
18
|
-
| **CLIProxyAPI Proxies** | **API Key / Bridge** | Upstream quota windows and reset intervals via server-side [`pi-bridge`](https://github.com/abix5/pi-cliproxyapi-bridge) |
|
|
13
|
+
The display follows the active model. It does not turn request failures into a zero balance.
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
## Supported providers
|
|
21
16
|
|
|
22
|
-
|
|
17
|
+
| Provider | Authentication | Available information |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| OpenAI Codex | ChatGPT Plus/Pro OAuth | 5-hour and 7-day quota windows |
|
|
20
|
+
| xAI / Grok | OAuth | Account verification and spending-limit status |
|
|
21
|
+
| Anthropic Claude | Claude OAuth or API key | Subscription status or API rate-limit headers |
|
|
22
|
+
| DeepSeek | API key | Account balance |
|
|
23
|
+
| GLM / Zhipu BigModel | API key | Coding Plan quota windows; standard pay-as-you-go keys cannot be queried through the official API |
|
|
24
|
+
| CLIProxyAPI | `pi-bridge` | Upstream quota windows and reset times |
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
- **Privacy & Security First**: Zero browser cookies, telemetry, or external proxies—reuses your local Pi credentials securely with strict same-origin protection.
|
|
26
|
+
Pi Usage uses Pi's existing provider authentication. CLIProxyAPI is queried through server-side [`pi-bridge`](https://github.com/abix5/pi-cliproxyapi-bridge); its Management Key is never read or stored by this plugin.
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
## Install
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
Choose one installation source.
|
|
30
31
|
|
|
31
|
-
###
|
|
32
|
+
### npm
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
In Pi's terminal:
|
|
34
35
|
|
|
35
36
|
```bash
|
|
36
37
|
pi install npm:@wayner6/pi-usage
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
In pi-web: open **Settings** → **Plugins** → **Add Plugin**, enter the following in **Source**, choose `global`, then select **Install**:
|
|
40
41
|
|
|
41
|
-
1. Open **Settings** (设置) at the bottom left -> **Plugins** (插件).
|
|
42
|
-
2. Click **+ Add Plugin** (添加插件).
|
|
43
|
-
3. In the **Source** input box, enter:
|
|
44
42
|
```text
|
|
45
43
|
npm:@wayner6/pi-usage
|
|
46
44
|
```
|
|
47
|
-
4. Choose `global` scope, then click **Install** (安装).
|
|
48
45
|
|
|
49
|
-
|
|
46
|
+
### GitHub
|
|
50
47
|
|
|
51
|
-
|
|
48
|
+
In Pi's terminal:
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
```bash
|
|
51
|
+
pi install github:wayner6/pi-usage
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
In pi-web: open **Settings** → **Plugins** → **Add Plugin**, enter the following in **Source**, choose `global`, then select **Install**:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
git:https://github.com/wayner6/pi-usage
|
|
58
|
+
```
|
|
61
59
|
|
|
62
|
-
|
|
60
|
+
After installing or updating the plugin, reload the session if it is already open.
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
## Use
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
| :--- | :--- |
|
|
68
|
-
| `/usage` | Show aggregated balance and quota details for all configured providers |
|
|
69
|
-
| `/usage current` | Show usage details specifically for the currently active model |
|
|
70
|
-
| `/usage refresh` | Force an immediate network refresh for the current provider |
|
|
71
|
-
| `/usage doctor` | Diagnose adapter states, provider authentication, and bridge connectivity |
|
|
72
|
-
| `/usage settings` | View or adjust configuration (status bar, widget, intervals) |
|
|
64
|
+
Send a normal message to refresh the quota for the active model. The status line then updates automatically.
|
|
73
65
|
|
|
74
|
-
|
|
66
|
+
In pi-web, an idle model switch is applied when the next message is sent. To refresh it immediately, use **Reload Session**.
|
|
67
|
+
|
|
68
|
+
Use these commands in the chat input:
|
|
69
|
+
|
|
70
|
+
| Command | Result |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `/usage` or `/quota` | Show all available provider balances and quotas |
|
|
73
|
+
| `/usage current` | Show the active model's provider only |
|
|
74
|
+
| `/usage refresh` | Refresh the active provider now |
|
|
75
|
+
| `/usage doctor` | Show authentication, adapter, and bridge diagnostics |
|
|
76
|
+
| `/usage settings` | Show current plugin settings |
|
|
77
|
+
|
|
78
|
+
## Settings
|
|
79
|
+
|
|
80
|
+
Settings are optional. Use `/usage settings` followed by one of these commands:
|
|
75
81
|
|
|
76
|
-
Adjust preferences directly in chat via `/usage settings`:
|
|
77
82
|
```text
|
|
78
|
-
/usage settings status on|off #
|
|
79
|
-
/usage settings widget on|off #
|
|
80
|
-
/usage settings interval <seconds> #
|
|
81
|
-
/usage settings timeout <seconds> #
|
|
83
|
+
/usage settings status on|off # status line; on by default
|
|
84
|
+
/usage settings widget on|off # detailed widget; off by default
|
|
85
|
+
/usage settings interval <seconds> # refresh interval; 120 by default
|
|
86
|
+
/usage settings timeout <seconds> # request timeout; 10 by default
|
|
82
87
|
```
|
|
83
|
-
Configuration is automatically saved to `~/.pi/agent/pi-usage/config.json`.
|
|
84
88
|
|
|
85
|
-
|
|
89
|
+
Settings are stored locally at `~/.pi/agent/pi-usage/config.json`.
|
|
90
|
+
|
|
91
|
+
## Privacy
|
|
92
|
+
|
|
93
|
+
The plugin does not use browser cookies, telemetry, cloud synchronization, or third-party credential forwarding. Requests to provider APIs stay on the provider's official origin.
|
|
86
94
|
|
|
87
95
|
## License
|
|
88
96
|
|
|
89
|
-
MIT
|
|
97
|
+
[MIT](./LICENSE)
|
package/package.json
CHANGED
package/src/core/types.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Metric, UsageAdapter, UsageSnapshot } from "../../../core/types.ts";
|
|
2
2
|
import { bridgeUrl, safeError, sameOriginFetch } from "../../../core/security.ts";
|
|
3
|
+
import { isAccountRelevantToModels } from "../matching.ts";
|
|
4
|
+
|
|
5
|
+
const NATIVE_PROVIDER_IDS = new Set(["deepseek", "openai-codex", "xai", "anthropic", "glm", "zai", "zai-coding-cn"]);
|
|
3
6
|
|
|
4
7
|
type BridgeGroup = { id?: string; label?: string; remainingFraction?: number; resetTime?: string; models?: Array<{ id?: string; displayName?: string; remainingFraction?: number; resetTime?: string }> };
|
|
5
8
|
type BridgeAccount = { provider?: string; account?: string; authIndex?: string; label?: string; status?: string; disabled?: boolean; unavailable?: boolean; supported?: boolean; error?: string; groups?: BridgeGroup[] };
|
|
@@ -18,17 +21,30 @@ export const cliProxyBridgeAdapter: UsageAdapter = {
|
|
|
18
21
|
id: "cliproxy-pi-bridge",
|
|
19
22
|
label: "CLIProxyAPI / pi-bridge",
|
|
20
23
|
canHandle(target) {
|
|
21
|
-
|
|
24
|
+
const pid = target.providerId.toLowerCase();
|
|
25
|
+
// Never hijack standard native providers unless explicitly configured as a bridge proxy
|
|
26
|
+
if (NATIVE_PROVIDER_IDS.has(pid)) return false;
|
|
27
|
+
|
|
28
|
+
// If explicit baseUrl exists, make sure it's not pointing to official provider APIs
|
|
22
29
|
if (target.baseUrl) {
|
|
23
30
|
try {
|
|
24
31
|
const url = new URL(target.baseUrl);
|
|
25
|
-
if (
|
|
32
|
+
if (
|
|
33
|
+
url.origin.includes("deepseek.com") ||
|
|
34
|
+
url.origin.includes("openai.com") ||
|
|
35
|
+
url.origin.includes("chatgpt.com") ||
|
|
36
|
+
url.origin.includes("anthropic.com") ||
|
|
37
|
+
url.origin.includes("x.ai") ||
|
|
38
|
+
url.origin.includes("bigmodel.cn")
|
|
39
|
+
) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
26
42
|
} catch {
|
|
27
43
|
return false;
|
|
28
44
|
}
|
|
29
45
|
}
|
|
30
|
-
|
|
31
|
-
//
|
|
46
|
+
|
|
47
|
+
// Accept if providerId hints at proxy/bridge or if any custom non-native baseUrl is present
|
|
32
48
|
return pid.includes("cpa") || pid.includes("cliproxy") || pid.includes("bridge") || pid.includes("proxy") || Boolean(target.baseUrl);
|
|
33
49
|
},
|
|
34
50
|
async fetch({ target, signal, force, fetchFn }): Promise<UsageSnapshot> {
|
|
@@ -48,7 +64,7 @@ export const cliProxyBridgeAdapter: UsageAdapter = {
|
|
|
48
64
|
if (!response.ok) throw new Error(`pi-bridge returned HTTP ${response.status}`);
|
|
49
65
|
const data = await response.json() as BridgeUsage;
|
|
50
66
|
if (data.schemaVersion !== 1) return { adapterId: this.id, sourceProviderId: target.providerId, displayName: target.providerId, state: "incompatible", fetchedAt, accounts: [], error: `Unsupported pi-bridge schemaVersion ${String(data.schemaVersion)}` };
|
|
51
|
-
|
|
67
|
+
let accounts = (data.accounts ?? []).map((account, index) => ({
|
|
52
68
|
id: account.authIndex ?? `${account.provider ?? "provider"}-${index}`,
|
|
53
69
|
provider: account.provider ?? "unknown",
|
|
54
70
|
label: account.label || account.account || account.provider || `Account ${index + 1}`,
|
|
@@ -59,6 +75,12 @@ export const cliProxyBridgeAdapter: UsageAdapter = {
|
|
|
59
75
|
rawGroups: account.groups,
|
|
60
76
|
...(account.error ? { error: account.error } : {}),
|
|
61
77
|
}));
|
|
78
|
+
|
|
79
|
+
// Filter accounts based on user-configured models for this provider in Pi
|
|
80
|
+
if (target.configuredModelIds && target.configuredModelIds.length > 0) {
|
|
81
|
+
accounts = accounts.filter((account) => isAccountRelevantToModels(account, target.configuredModelIds));
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
return {
|
|
63
85
|
adapterId: this.id,
|
|
64
86
|
sourceProviderId: target.providerId,
|
|
@@ -22,16 +22,30 @@ export class ProviderUsageController {
|
|
|
22
22
|
|
|
23
23
|
setConfig(config: UsageConfig): void { this.config = config; }
|
|
24
24
|
|
|
25
|
-
async target(ctx: ExtensionContext, providerId: string, model
|
|
25
|
+
async target(ctx: ExtensionContext, providerId: string, model?: Model<Api>): Promise<ProviderTarget> {
|
|
26
26
|
const provider = ctx.modelRegistry.getProvider(providerId);
|
|
27
27
|
const auth = await ctx.modelRegistry.getProviderAuth(providerId);
|
|
28
|
-
|
|
28
|
+
|
|
29
|
+
// Only associate the active model if it actually belongs to this provider!
|
|
30
|
+
const activeModel = model ?? ctx.model;
|
|
31
|
+
const matchedModel = activeModel?.provider?.toLowerCase() === providerId.toLowerCase() ? activeModel : undefined;
|
|
32
|
+
|
|
33
|
+
// Base URL resolution: NEVER inherit baseUrl from a foreign provider's model!
|
|
34
|
+
const baseUrl = auth?.auth.baseUrl ?? matchedModel?.baseUrl ?? provider?.baseUrl;
|
|
35
|
+
|
|
36
|
+
// Collect all models configured in Pi under this specific provider
|
|
37
|
+
const allModels = ctx.modelRegistry.getAll();
|
|
38
|
+
const configuredModelIds = allModels
|
|
39
|
+
.filter((m) => m.provider?.toLowerCase() === providerId.toLowerCase())
|
|
40
|
+
.map((m) => m.id);
|
|
41
|
+
|
|
29
42
|
return {
|
|
30
43
|
providerId,
|
|
31
|
-
...(
|
|
44
|
+
...(matchedModel ? { model: matchedModel } : {}),
|
|
32
45
|
...(provider ? { provider } : {}),
|
|
33
46
|
...(auth ? { auth } : {}),
|
|
34
47
|
...(baseUrl ? { baseUrl } : {}),
|
|
48
|
+
...(configuredModelIds.length ? { configuredModelIds } : {}),
|
|
35
49
|
};
|
|
36
50
|
}
|
|
37
51
|
|
|
@@ -226,3 +226,67 @@ export function matchModelGroup(
|
|
|
226
226
|
const res = matchModelAcrossAccounts([{ rawGroups: groups }], modelId);
|
|
227
227
|
return res?.quota;
|
|
228
228
|
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Determines whether a proxy account (e.g. from pi-bridge) is relevant to the models
|
|
232
|
+
* actually configured by the user for this provider in Pi.
|
|
233
|
+
* If configuredModelIds is empty or omitted, all accounts are considered relevant.
|
|
234
|
+
*/
|
|
235
|
+
export function isAccountRelevantToModels(
|
|
236
|
+
account: { provider?: string; rawGroups?: unknown },
|
|
237
|
+
configuredModelIds?: string[],
|
|
238
|
+
): boolean {
|
|
239
|
+
if (!configuredModelIds || configuredModelIds.length === 0) return true;
|
|
240
|
+
|
|
241
|
+
const targetTokens = new Set(configuredModelIds.flatMap(tokenizeModelId));
|
|
242
|
+
const groups = Array.isArray(account.rawGroups) ? (account.rawGroups as RawBridgeGroup[]) : [];
|
|
243
|
+
const provTokens = tokenizeModelId(account.provider ?? "");
|
|
244
|
+
|
|
245
|
+
// 1. Direct provider match or overlap with configured model tokens
|
|
246
|
+
// E.g. account.provider === "codex" vs models having "codex", or "antigravity" vs "gemini"/"claude"
|
|
247
|
+
const provNormalized = (account.provider ?? "").toLowerCase();
|
|
248
|
+
for (const mid of configuredModelIds) {
|
|
249
|
+
const mLower = mid.toLowerCase();
|
|
250
|
+
if (provNormalized && (mLower.includes(provNormalized) || provNormalized.includes(mLower))) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check token intersection with account provider
|
|
256
|
+
if (provTokens.some((t) => targetTokens.has(t))) return true;
|
|
257
|
+
|
|
258
|
+
// 2. Check groups and their inner models
|
|
259
|
+
const modelKeywords = [
|
|
260
|
+
"claude", "gemini", "codex", "gpt", "openai", "deepseek", "kimi", "moonshot", "grok", "xai",
|
|
261
|
+
"thinking", "flash", "pro", "opus", "sonnet", "haiku", "turbo", "mini", "reasoning", "antigravity",
|
|
262
|
+
];
|
|
263
|
+
for (const group of groups) {
|
|
264
|
+
const gTokens = tokenizeModelId(`${group.id ?? ""} ${group.label ?? ""}`);
|
|
265
|
+
const gOverlap = gTokens.filter((t) => targetTokens.has(t));
|
|
266
|
+
if (gOverlap.some((t) => modelKeywords.includes(t)) || gOverlap.length >= 2) return true;
|
|
267
|
+
|
|
268
|
+
for (const m of group.models ?? []) {
|
|
269
|
+
const mid = (m.id ?? "").trim().toLowerCase();
|
|
270
|
+
if (!mid) continue;
|
|
271
|
+
|
|
272
|
+
for (const targetId of configuredModelIds) {
|
|
273
|
+
const tid = targetId.trim().toLowerCase();
|
|
274
|
+
if (mid === tid || tid.includes(mid) || mid.includes(tid)) return true;
|
|
275
|
+
|
|
276
|
+
const mTokens = tokenizeModelId(mid);
|
|
277
|
+
const overlap = mTokens.filter((t) => targetTokens.has(t));
|
|
278
|
+
if (overlap.length >= 2 || overlap.some((t) => modelKeywords.includes(t))) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 3. Provider abbreviation match (e.g. "ag-" models vs "antigravity" provider)
|
|
286
|
+
if (provNormalized === "antigravity" && configuredModelIds.some((id) => id.toLowerCase().startsWith("ag-"))) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|