@wayner6/pi-usage 0.1.0 → 0.1.2

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
- **Real-time provider balances, quota windows, and rate limits for [Pi](https://github.com/earendil-works/pi-mono) and [pi-web](https://github.com/agegr/pi-web).**
3
+ [中文文档](./README_zh.md)
4
4
 
5
- [简体中文文档](./README_zh.md)
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
- ## Supported Providers & Authentication Types
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
- | Provider / Target | Auth Type | Monitored Quota / Balance Data |
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
- ## Key Features
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
- - **Real-Time Quota & Multi-Window Tracking**: Automatically monitors balances, subscription states, and multiple sliding windows (e.g. `Codex 5h 92% · 7d 85%`) tailored to your currently active model.
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
- ## Installation
30
+ Choose one installation source.
30
31
 
31
- ### Method 1: Install in Pi (CLI / TUI)
32
+ ### npm
32
33
 
33
- Run the following command directly in your terminal:
34
+ In Pi's terminal:
34
35
 
35
36
  ```bash
36
37
  pi install npm:@wayner6/pi-usage
37
38
  ```
38
39
 
39
- ### Method 2: Install in pi-web (Web Interface)
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
- ## Usage & Refresh Behavior
48
+ In Pi's terminal:
52
49
 
53
- ### Refresh Mechanism
54
- 1. **Automatic Refresh on Dialogue**:
55
- - In both `pi` and `pi-web`, **starting a conversation (sending any prompt) automatically refreshes and streams the latest balance/quota** for the active model.
56
- 2. **Model Switching in pi-web**:
57
- - When switching models while the agent is idle, the footer quota updates **automatically on your next prompt**.
58
- - To refresh immediately without sending a message, click **"Reload Session"** (重载会话) in the menu or sidebar.
59
- 3. **Background Updates**:
60
- - Polls and updates countdowns periodically in the background (default: every 120s).
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
- ### Available Commands
60
+ After installing or updating the plugin, reload the session if it is already open.
63
61
 
64
- Type `/usage` or `/quota` in the chat input:
62
+ ## Use
65
63
 
66
- | Command | Description |
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
- ### Configuration Options
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 # Toggle the compact footer status line (Default: on)
79
- /usage settings widget on|off # Toggle the detailed persistent widget (Default: off)
80
- /usage settings interval <seconds> # Set background polling interval (Default: 120s)
81
- /usage settings timeout <seconds> # Set request timeout (Default: 10s)
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 License. See [LICENSE](./LICENSE) for details.
97
+ [MIT](./LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wayner6/pi-usage",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/core/types.ts CHANGED
@@ -50,6 +50,7 @@ export interface ProviderTarget {
50
50
  provider?: Provider<Api>;
51
51
  baseUrl?: string;
52
52
  auth?: AuthResult;
53
+ configuredModelIds?: string[];
53
54
  }
54
55
 
55
56
  export interface FetchContext {
@@ -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
- // 1. If explicit baseUrl exists and is not official deepseek/openai, it's a potential bridge target
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 (url.origin.includes("deepseek.com")) return false;
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
- const pid = target.providerId.toLowerCase();
31
- // 2. Accept if providerId hints at proxy/bridge or if any custom baseUrl is present
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
- const accounts = (data.accounts ?? []).map((account, index) => ({
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 = ctx.model): Promise<ProviderTarget> {
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
- const baseUrl = auth?.auth.baseUrl ?? model?.baseUrl ?? provider?.baseUrl;
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
- ...(model?.provider === providerId ? { model } : {}),
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
+