@cad0p/pi-usage 0.11.6-20261107.0 → 0.11.6
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 +6 -14
- package/bin/piquota.js +1 -1
- package/extensions/quota-panel.ts +0 -9
- package/package.json +2 -3
- package/src/auth/pi-auth.js +0 -2
- package/src/cli/explain.js +0 -2
- package/src/engine.js +1 -3
- package/src/omarchy/record.js +5 -14
- package/src/render/panel.js +0 -9
- package/omarchy/cad0p.pi-usage/assets/mistral.svg +0 -18
- package/src/omarchy/assets/mistral.svg +0 -18
- package/src/providers/mistral.js +0 -251
package/README.md
CHANGED
|
@@ -6,7 +6,6 @@ One source of truth for quota: **the credentials Pi already owns**.
|
|
|
6
6
|
> `@cad0p/pi-usage` is a fork of [J3pf/piQuota](https://github.com/J3pf/piQuota) (`@j3pf/piquota`). It adds:
|
|
7
7
|
>
|
|
8
8
|
> * **OpenCode Go from the API key**: `GET https://opencode.ai/zen/go/v1/usage` returns the rolling (5h), weekly and monthly windows for the key Pi already stores, so no browser session is needed.
|
|
9
|
-
> * **Mistral from the API key**: `GET https://api.mistral.ai/v1/billing/budget` and `/v1/billing/subscription`, the two reads the Vibe CLI makes for its usage gauge, return the monthly Vibe Code and API budgets and the plan for the key Pi already stores (`mistral`, and siblings such as `mistral-1-api`). They are read-only and cost no tokens.
|
|
10
9
|
> * **Numbered accounts**: `auth.json` entries such as `opencode-go-2` … `opencode-go-14` (the [pi-fallback-provider](https://github.com/cad0p/pi-fallback-provider) sibling convention) are read as extra accounts of their base provider. The accounts of one provider are queried in parallel (8 at a time, `PI_QUOTA_ACCOUNT_CONCURRENCY` from 1 to 32), so 14 OpenCode Go keys cost about one round trip instead of fourteen. Every request carries `User-Agent: pi-usage/<version> (+https://github.com/cad0p/pi-usage)` unless a provider needs its own client string: OpenCode Go asks clients not to use a generic HTTP-library name, and Node's default is `node`.
|
|
11
10
|
> * **One Omarchy tab per provider**: a multi-account provider shows each account's binding window (`#2 · Monthly`) in a single tab, and summarises failed accounts.
|
|
12
11
|
> * **Tokens by day and by model**: the tokens Pi's own session files recorded are folded per provider and shown in `/usage`, in `--json` (`localStats`) and in the Omarchy records, at a bounded scan cost.
|
|
@@ -84,7 +83,6 @@ shuvquota, and it never writes to a credential file.
|
|
|
84
83
|
| Codex | `openai-codex.access` + `accountId` | `GET chatgpt.com/backend-api/wham/usage` | ✅ 5h + weekly + plan |
|
|
85
84
|
| Antigravity | `google-antigravity.access` (or legacy `antigravity`) + `projectId` | `POST cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary` | ✅ Gemini + Claude/GPT buckets, **with in-memory refresh** |
|
|
86
85
|
| OpenCode Go | `opencode-go.key`, numbered siblings included | `GET opencode.ai/zen/go/v1/usage` | ✅ 5h + weekly + monthly per account; the session cookie and SSR dashboard are a base-account fallback |
|
|
87
|
-
| Mistral | `mistral.key`, numbered siblings included | `GET api.mistral.ai/v1/billing/budget` + `/v1/billing/subscription` | ✅ Vibe monthly + API monthly + plan per key; without the `see_org_inference_usage` permission the plan alone, with a note. Keys of one organisation share its budgets, so they show the same numbers |
|
|
88
86
|
| GitHub Actions (opt-in) | Authenticated `gh` CLI | `gh api /organizations/<org>/settings/billing/usage/summary?product=Actions` | ✅ org-wide monthly Linux minutes, plan-derived allowance |
|
|
89
87
|
|
|
90
88
|
Four findings worth recording, each of which cost a wrong hypothesis:
|
|
@@ -386,8 +384,7 @@ right rail instead, under Status and TODO, and the above-editor box stays quiet
|
|
|
386
384
|
the numbers are not shown twice. See [the rail slot](#the-gentle-pi-rail-slot).
|
|
387
385
|
|
|
388
386
|
* each name is painted with its **own brand colour** — Claude `#D97757`, Codex
|
|
389
|
-
`#10A37F`, Antigravity `#4285F4`, OpenCode `#007AFF
|
|
390
|
-
the Vibe budget, appears only when a Mistral key is configured);
|
|
387
|
+
`#10A37F`, Antigravity `#4285F4`, OpenCode `#007AFF`;
|
|
391
388
|
* the numbers are **used**, not remaining, and the semaphore is both colour and
|
|
392
389
|
shape (`○` plenty left → `◔` → `◕` → `●` nearly spent), so it still reads on a
|
|
393
390
|
colourblind or mono terminal;
|
|
@@ -530,10 +527,6 @@ Rp suite) without external network daemons or closed-source intermediaries:
|
|
|
530
527
|
}
|
|
531
528
|
```
|
|
532
529
|
|
|
533
|
-
A provider may also carry a top-level `note` when it answered but has no windows
|
|
534
|
-
to show, such as a Mistral key without the `see_org_inference_usage` permission
|
|
535
|
-
(`"ok": true`, `"windows": []`, the plan, and the reason in `note`).
|
|
536
|
-
|
|
537
530
|
The token part of the same report is abridged here (`recentDays` always carries
|
|
538
531
|
seven entries, oldest first, today last):
|
|
539
532
|
|
|
@@ -570,7 +563,7 @@ stale without triggering rate limits:
|
|
|
570
563
|
| Family | Clock |
|
|
571
564
|
| --- | --- |
|
|
572
565
|
| Claude | **300 s** (`--claude-ttl`) |
|
|
573
|
-
| Codex, Antigravity, OpenCode Go
|
|
566
|
+
| Codex, Antigravity, OpenCode Go | **60 s** (`--fetch-ttl`) |
|
|
574
567
|
|
|
575
568
|
`src/refresh.js` keeps a timestamp per family, requests only what is past its own
|
|
576
569
|
clock, and merges the result back into the canonical report. `src/sticky.js`
|
|
@@ -583,7 +576,7 @@ credential) are never masked.
|
|
|
583
576
|
Omarchy 4.0.4's **Agents** bar panel draws every `*.json` record in
|
|
584
577
|
`${XDG_STATE_HOME:-~/.local/state}/omarchy/agents/usage/`, whoever wrote it.
|
|
585
578
|
`piquota omarchy` (opt-in, one shot) writes one record per provider piQuota can
|
|
586
|
-
read, so Claude, Codex, Antigravity
|
|
579
|
+
read, so Claude, Codex, Antigravity and OpenCode Go all get a tab:
|
|
587
580
|
|
|
588
581
|
```bash
|
|
589
582
|
piquota omarchy # write pi-claude.json, pi-codex.json, ... once
|
|
@@ -706,7 +699,7 @@ What counts: one assistant message with a non-zero token count is one prompt,
|
|
|
706
699
|
and its tokens are `input + output + cacheRead + cacheWrite` — the same sum the
|
|
707
700
|
Omarchy panel renders. Messages are bucketed by **local** date (the panel calls
|
|
708
701
|
the last row "Today"). Only providers piQuota can bill are counted:
|
|
709
|
-
`anthropic`, `openai-codex`, `antigravity`, `opencode-go
|
|
702
|
+
`anthropic`, `openai-codex`, `antigravity`, `opencode-go` and their numbered
|
|
710
703
|
siblings, so `opencode-go-7` is account `#7`. Providers it does not manage
|
|
711
704
|
(`deepinfra`, `xai`, ...) are ignored, and so is the custom `claude-bridge`
|
|
712
705
|
provider: its tokens are not a Pi subscription.
|
|
@@ -875,13 +868,12 @@ extensions/quota-panel.ts Pi TUI line and /usage
|
|
|
875
868
|
## Tests
|
|
876
869
|
|
|
877
870
|
```bash
|
|
878
|
-
node --test tests/*.test.mjs #
|
|
871
|
+
node --test tests/*.test.mjs # 455 tests, fake tokens only, no network
|
|
879
872
|
```
|
|
880
873
|
|
|
881
874
|
Modules covered: `auth.json` parsing and de-duplication, the Claude Code store
|
|
882
875
|
(including that the refresh token never leaves it and that reading leaves the file
|
|
883
|
-
byte-identical), Claude source precedence, the
|
|
884
|
-
the missing usage permission and a rejected key, GitHub
|
|
876
|
+
byte-identical), Claude source precedence, the five providers (including GitHub
|
|
885
877
|
Actions plan resolution, opt-in gating and degradation, the two Antigravity
|
|
886
878
|
failures and the OpenCode degradation), the dashboard parser's three
|
|
887
879
|
strategies, the Firefox cookie reader against a synthetic SQLite database, the
|
package/bin/piquota.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* piquota — independent, read-only quota for the credentials Pi already owns.
|
|
4
4
|
*
|
|
5
|
-
* piquota panel for Claude, Codex, Antigravity, OpenCode Go
|
|
5
|
+
* piquota panel for Claude, Codex, Antigravity, OpenCode Go
|
|
6
6
|
* piquota --json normalized report (used by rp-host / tools)
|
|
7
7
|
* piquota --compact | --status | --explain
|
|
8
8
|
* piquota auth opencode open the OpenCode login and capture the session
|
|
@@ -187,7 +187,6 @@ const BRAND: Record<string, string> = {
|
|
|
187
187
|
codex: "#10A37F",
|
|
188
188
|
antigravity: "#4285F4",
|
|
189
189
|
"opencode-go": "#007AFF",
|
|
190
|
-
mistral: "#FA520F",
|
|
191
190
|
"github-actions": "#2088FF",
|
|
192
191
|
};
|
|
193
192
|
|
|
@@ -197,7 +196,6 @@ const SHORT_NAME: Record<string, string> = {
|
|
|
197
196
|
codex: "Codex",
|
|
198
197
|
antigravity: "Agy",
|
|
199
198
|
"opencode-go": "OP-Go",
|
|
200
|
-
mistral: "Mistral",
|
|
201
199
|
"github-actions": "GH",
|
|
202
200
|
};
|
|
203
201
|
|
|
@@ -471,9 +469,6 @@ export function renderBox(report: QuotaReport, theme: Theme, totalWidth?: number
|
|
|
471
469
|
{ label: "Agy C/G:", color: "#7AA2F7", ...windowFor("antigravity", (window) => window.id === "claude-gpt-5h" || (/(claude|gpt|3p)/i.test(window.id) && isFiveHour(window))) },
|
|
472
470
|
{ label: "OP-Go: ", color: "#007AFF", ...windowFor("opencode-go", isFiveHour) },
|
|
473
471
|
];
|
|
474
|
-
if (providerFor("mistral") && providerFor("mistral")?.notConfigured !== true) {
|
|
475
|
-
rows.push({ label: "Mistral:", color: "#FA520F", ...windowFor("mistral", (window) => window.id === "vibe") });
|
|
476
|
-
}
|
|
477
472
|
if (providerFor("github-actions")) {
|
|
478
473
|
rows.push({ label: "GH Actions:", color: "#2088FF", ...windowFor("github-actions", (window) => window.id === "monthly") });
|
|
479
474
|
}
|
|
@@ -523,9 +518,6 @@ function renderSidebarCard(report: QuotaReport | null, theme: Theme, width: numb
|
|
|
523
518
|
{ label: "Agy: ", color: "#4285F4", ...windowFor("antigravity", (window) => window.id === "gemini-5h" || (/gemini/i.test(window.id) && isFiveHour(window))) },
|
|
524
519
|
{ label: "Agy C/G:", color: "#7AA2F7", ...windowFor("antigravity", (window) => window.id === "claude-gpt-5h" || (/(claude|gpt|3p)/i.test(window.id) && isFiveHour(window))) },
|
|
525
520
|
{ label: "OP-Go: ", color: "#007AFF", ...windowFor("opencode-go", isFiveHour) },
|
|
526
|
-
...(providerFor("mistral") && providerFor("mistral")?.notConfigured !== true
|
|
527
|
-
? [{ label: "Mistral:", color: "#FA520F", ...windowFor("mistral", (window) => window.id === "vibe") }]
|
|
528
|
-
: []),
|
|
529
521
|
...(providerFor("github-actions")
|
|
530
522
|
? [{ label: "GH Actions:", color: "#2088FF", ...windowFor("github-actions", (window) => window.id === "monthly") }]
|
|
531
523
|
: []),
|
|
@@ -1138,7 +1130,6 @@ const FAMILY_LABEL: Record<string, string> = {
|
|
|
1138
1130
|
codex: "Codex (Pi)",
|
|
1139
1131
|
antigravity: "Antigravity (Pi)",
|
|
1140
1132
|
"opencode-go": "OpenCode Go (Pi)",
|
|
1141
|
-
mistral: "Mistral (Pi)",
|
|
1142
1133
|
"github-actions": "GitHub Actions",
|
|
1143
1134
|
};
|
|
1144
1135
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cad0p/pi-usage",
|
|
3
|
-
"version": "0.11.6
|
|
3
|
+
"version": "0.11.6",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
7
|
"type": "module",
|
|
8
|
-
"description": "Read-only usage/quota for the AI subscriptions Pi already owns (Claude, Codex, multi-account OpenCode Go, Antigravity
|
|
8
|
+
"description": "Read-only usage/quota for the AI subscriptions Pi already owns (Claude, Codex, multi-account OpenCode Go, Antigravity): /usage in Pi and the Omarchy Agents panel. Fork of @j3pf/piquota.",
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=22.19.0"
|
|
11
11
|
},
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
"usage",
|
|
35
35
|
"omarchy",
|
|
36
36
|
"opencode-go",
|
|
37
|
-
"mistral",
|
|
38
37
|
"multi-account"
|
|
39
38
|
],
|
|
40
39
|
"repository": {
|
package/src/auth/pi-auth.js
CHANGED
|
@@ -28,7 +28,6 @@ export const FAMILY_BY_PROVIDER = {
|
|
|
28
28
|
// kept for stores written by older Pi releases and third-party plugins.
|
|
29
29
|
"google-antigravity": "antigravity",
|
|
30
30
|
"opencode-go": "opencode-go",
|
|
31
|
-
mistral: "mistral",
|
|
32
31
|
};
|
|
33
32
|
|
|
34
33
|
/** Display label used by every surface (terminal, Pi TUI, Moshi cards). */
|
|
@@ -37,7 +36,6 @@ export const LABEL_BY_FAMILY = {
|
|
|
37
36
|
claude: "Claude (Pi)",
|
|
38
37
|
antigravity: "Antigravity (Pi)",
|
|
39
38
|
"opencode-go": "OpenCode Go (Pi)",
|
|
40
|
-
mistral: "Mistral (Pi)",
|
|
41
39
|
"github-actions": "GitHub Actions",
|
|
42
40
|
};
|
|
43
41
|
|
package/src/cli/explain.js
CHANGED
package/src/engine.js
CHANGED
|
@@ -16,11 +16,10 @@ import { fetchQuota as fetchClaude } from "./providers/claude.js";
|
|
|
16
16
|
import { fetchQuota as fetchCodex } from "./providers/codex.js";
|
|
17
17
|
import { fetchQuota as fetchAntigravity } from "./providers/antigravity.js";
|
|
18
18
|
import { fetchQuota as fetchOpenCodeGo } from "./providers/opencode-go.js";
|
|
19
|
-
import { fetchQuota as fetchMistral } from "./providers/mistral.js";
|
|
20
19
|
import { fetchQuota as fetchGithubActions } from "./providers/github-actions.js";
|
|
21
20
|
|
|
22
21
|
/** Canonical family order used by every surface. */
|
|
23
|
-
export const FAMILIES = ["claude", "codex", "antigravity", "opencode-go", "
|
|
22
|
+
export const FAMILIES = ["claude", "codex", "antigravity", "opencode-go", "github-actions"];
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
25
|
* Return the ordered families enabled for a run.
|
|
@@ -197,7 +196,6 @@ const PROVIDERS = {
|
|
|
197
196
|
codex: fetchCodex,
|
|
198
197
|
antigravity: fetchAntigravity,
|
|
199
198
|
"opencode-go": fetchOpenCodeGo,
|
|
200
|
-
mistral: fetchMistral,
|
|
201
199
|
"github-actions": fetchGithubActions,
|
|
202
200
|
};
|
|
203
201
|
|
package/src/omarchy/record.js
CHANGED
|
@@ -38,7 +38,6 @@ const LOGIN_HINT = {
|
|
|
38
38
|
codex: "Run `/login openai-codex` in Pi to restore usage.",
|
|
39
39
|
antigravity: "Run `/login antigravity` in Pi to restore usage.",
|
|
40
40
|
"opencode-go": "Run `piquota auth opencode` to restore usage.",
|
|
41
|
-
mistral: "Run `/login mistral` in Pi to restore usage.",
|
|
42
41
|
};
|
|
43
42
|
|
|
44
43
|
const HELP_MAX = 200;
|
|
@@ -119,11 +118,9 @@ function mapLimits(windows, provider, prefix = "") {
|
|
|
119
118
|
for (const window of windows ?? []) {
|
|
120
119
|
// A window without a percentage would otherwise read as "0% used".
|
|
121
120
|
if (typeof window.usedPercent !== "number" || !Number.isFinite(window.usedPercent)) continue;
|
|
122
|
-
// Spending past the allowance changes what 100% means, so the title says so.
|
|
123
|
-
const payg = typeof window.note === "string" && /pay-as-you-go/i.test(window.note) ? " · pay-as-you-go" : "";
|
|
124
121
|
limits.push({
|
|
125
122
|
label: redact(`${prefix}${window.label || window.id || "Limit"}`),
|
|
126
|
-
title: redact(`${prefix}${windowTitle(window, provider)}
|
|
123
|
+
title: redact(`${prefix}${windowTitle(window, provider)}`),
|
|
127
124
|
percent: Math.min(1, Math.max(0, window.usedPercent / 100)),
|
|
128
125
|
resetsAt: typeof window.resetsAt === "string" ? window.resetsAt : "",
|
|
129
126
|
});
|
|
@@ -387,11 +384,7 @@ function buildFamilyRecord(family, providers, options = {}) {
|
|
|
387
384
|
const window = bindingWindow(provider.windows);
|
|
388
385
|
const mapped = window ? mapLimits([window], provider, label === "" ? "" : `${label} · `)[0] : null;
|
|
389
386
|
if (mapped) limits.push(mapped);
|
|
390
|
-
|
|
391
|
-
// An account that answered without windows (Mistral without the usage
|
|
392
|
-
// permission) says why instead of showing an empty row.
|
|
393
|
-
const quiet = accountLimits.length === 0 && typeof provider.note === "string" ? redact(provider.note).slice(0, HELP_MAX) : "";
|
|
394
|
-
accounts.push({ id: redact(label), ok: true, status: quiet, limits: accountLimits });
|
|
387
|
+
accounts.push({ id: redact(label), ok: true, status: "", limits: mapLimits(provider.windows, provider) });
|
|
395
388
|
return;
|
|
396
389
|
}
|
|
397
390
|
failures.push({ label, reason: failureReason(provider.error) });
|
|
@@ -488,11 +481,9 @@ export function buildRecord(provider, options = {}) {
|
|
|
488
481
|
|
|
489
482
|
if (failed && errorKind(provider.error) === "missing") return null;
|
|
490
483
|
|
|
491
|
-
const status =
|
|
492
|
-
?
|
|
493
|
-
:
|
|
494
|
-
? describeError(provider.family, provider.error ?? "no usage windows")
|
|
495
|
-
: { usageStatusText: "", authHelpText: "" };
|
|
484
|
+
const status = failed
|
|
485
|
+
? describeError(provider.family, provider.error ?? "no usage windows")
|
|
486
|
+
: { usageStatusText: "", authHelpText: "" };
|
|
496
487
|
|
|
497
488
|
const id = recordId(provider.family);
|
|
498
489
|
return {
|
package/src/render/panel.js
CHANGED
|
@@ -15,7 +15,6 @@ const FAMILY_SHORT = {
|
|
|
15
15
|
codex: "X",
|
|
16
16
|
antigravity: "A",
|
|
17
17
|
"opencode-go": "G",
|
|
18
|
-
mistral: "M",
|
|
19
18
|
"github-actions": "GH",
|
|
20
19
|
};
|
|
21
20
|
|
|
@@ -24,7 +23,6 @@ const FAMILY_TITLE = {
|
|
|
24
23
|
codex: "Codex (Pi)",
|
|
25
24
|
antigravity: "Antigravity (Pi)",
|
|
26
25
|
"opencode-go": "OpenCode Go (Pi)",
|
|
27
|
-
mistral: "Mistral (Pi)",
|
|
28
26
|
"github-actions": "GitHub Actions",
|
|
29
27
|
};
|
|
30
28
|
|
|
@@ -220,8 +218,6 @@ export function renderProviderLines(provider, paint, contentWidth = MAX_WIDTH, o
|
|
|
220
218
|
}
|
|
221
219
|
|
|
222
220
|
if ((provider.windows ?? []).length === 0) {
|
|
223
|
-
// A provider that answered but cannot show windows says why in its note.
|
|
224
|
-
if (provider.note) lines.push(` ${paint("warning", `~ ${clip(provider.note, contentWidth - 4)}`)}`);
|
|
225
221
|
lines.push(` ${paint("dim", "no rate-limit windows reported")}`);
|
|
226
222
|
return lines;
|
|
227
223
|
}
|
|
@@ -289,11 +285,6 @@ export function renderBoxWidget(providers, paint, totalWidth = typeof process?.s
|
|
|
289
285
|
{ label: "Agy C/G:", ...windowFor("antigravity", (window) => window.id === "claude-gpt-5h" || (/(claude|gpt|3p)/i.test(window.id) && isFiveHour(window))) },
|
|
290
286
|
{ label: "OP-Go: ", ...windowFor("opencode-go") },
|
|
291
287
|
];
|
|
292
|
-
// Mistral joins the compact widget only when it is configured, so a user
|
|
293
|
-
// without it never sees a permanent "not configured" row.
|
|
294
|
-
if (providerFor("mistral") && providerFor("mistral").notConfigured !== true) {
|
|
295
|
-
rows.push({ label: "Mistral:", ...windowFor("mistral", (window) => window.id === "vibe") });
|
|
296
|
-
}
|
|
297
288
|
if (providerFor("github-actions")) {
|
|
298
289
|
rows.push({ label: "GH Actions:", ...windowFor("github-actions", (window) => window.id === "monthly") });
|
|
299
290
|
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 10 10" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<rect x="0" y="0" width="2" height="2" fill="#FFD800"/>
|
|
3
|
-
<rect x="8" y="0" width="2" height="2" fill="#FFD800"/>
|
|
4
|
-
<rect x="0" y="2" width="2" height="2" fill="#FFAF00"/>
|
|
5
|
-
<rect x="2" y="2" width="2" height="2" fill="#FFAF00"/>
|
|
6
|
-
<rect x="6" y="2" width="2" height="2" fill="#FFAF00"/>
|
|
7
|
-
<rect x="8" y="2" width="2" height="2" fill="#FFAF00"/>
|
|
8
|
-
<rect x="0" y="4" width="2" height="2" fill="#FF8205"/>
|
|
9
|
-
<rect x="2" y="4" width="2" height="2" fill="#FF8205"/>
|
|
10
|
-
<rect x="4" y="4" width="2" height="2" fill="#FF8205"/>
|
|
11
|
-
<rect x="6" y="4" width="2" height="2" fill="#FF8205"/>
|
|
12
|
-
<rect x="8" y="4" width="2" height="2" fill="#FF8205"/>
|
|
13
|
-
<rect x="0" y="6" width="2" height="2" fill="#FA500F"/>
|
|
14
|
-
<rect x="4" y="6" width="2" height="2" fill="#FA500F"/>
|
|
15
|
-
<rect x="8" y="6" width="2" height="2" fill="#FA500F"/>
|
|
16
|
-
<rect x="0" y="8" width="2" height="2" fill="#E10500"/>
|
|
17
|
-
<rect x="8" y="8" width="2" height="2" fill="#E10500"/>
|
|
18
|
-
</svg>
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 10 10" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<rect x="0" y="0" width="2" height="2" fill="#FFD800"/>
|
|
3
|
-
<rect x="8" y="0" width="2" height="2" fill="#FFD800"/>
|
|
4
|
-
<rect x="0" y="2" width="2" height="2" fill="#FFAF00"/>
|
|
5
|
-
<rect x="2" y="2" width="2" height="2" fill="#FFAF00"/>
|
|
6
|
-
<rect x="6" y="2" width="2" height="2" fill="#FFAF00"/>
|
|
7
|
-
<rect x="8" y="2" width="2" height="2" fill="#FFAF00"/>
|
|
8
|
-
<rect x="0" y="4" width="2" height="2" fill="#FF8205"/>
|
|
9
|
-
<rect x="2" y="4" width="2" height="2" fill="#FF8205"/>
|
|
10
|
-
<rect x="4" y="4" width="2" height="2" fill="#FF8205"/>
|
|
11
|
-
<rect x="6" y="4" width="2" height="2" fill="#FF8205"/>
|
|
12
|
-
<rect x="8" y="4" width="2" height="2" fill="#FF8205"/>
|
|
13
|
-
<rect x="0" y="6" width="2" height="2" fill="#FA500F"/>
|
|
14
|
-
<rect x="4" y="6" width="2" height="2" fill="#FA500F"/>
|
|
15
|
-
<rect x="8" y="6" width="2" height="2" fill="#FA500F"/>
|
|
16
|
-
<rect x="0" y="8" width="2" height="2" fill="#E10500"/>
|
|
17
|
-
<rect x="8" y="8" width="2" height="2" fill="#E10500"/>
|
|
18
|
-
</svg>
|
package/src/providers/mistral.js
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mistral quota.
|
|
3
|
-
*
|
|
4
|
-
* Pi's built-in `mistral` provider stores an API key (a Vibe key or a
|
|
5
|
-
* la Plateforme key; Mistral accepts either on the same API). The plan's usage
|
|
6
|
-
* is served by Mistral's public billing API, the same two reads the Vibe CLI
|
|
7
|
-
* makes for its footer gauge (`vibe/app_server/_billing.py`):
|
|
8
|
-
*
|
|
9
|
-
* GET /v1/billing/subscription -> plan_name, payg_enabled, vibe_payg_enabled
|
|
10
|
-
* GET /v1/billing/budget -> vibe_budget, api_budget: usage_percentage,
|
|
11
|
-
* initial_budget, currency, reset_at, payg_enabled
|
|
12
|
-
*
|
|
13
|
-
* Both are read-only GETs that cost no tokens. `/budget` needs the
|
|
14
|
-
* `see_org_inference_usage` permission, so a member without it still gets the
|
|
15
|
-
* plan, with a note instead of windows. The key is sent only as a Bearer header
|
|
16
|
-
* to api.mistral.ai and never appears in a result.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { buildWindow, clampPercent, degradedResult, stringValue } from "../model.js";
|
|
20
|
-
import { requestJson } from "../http.js";
|
|
21
|
-
|
|
22
|
-
const API_BASE = "https://api.mistral.ai/v1";
|
|
23
|
-
const SUBSCRIPTION_URL = `${API_BASE}/billing/subscription`;
|
|
24
|
-
const BUDGET_URL = `${API_BASE}/billing/budget`;
|
|
25
|
-
|
|
26
|
-
/** A monthly budget, in the order every surface lists them. */
|
|
27
|
-
const BUDGETS = [
|
|
28
|
-
{ field: "vibe_budget", id: "vibe", label: "Vibe monthly" },
|
|
29
|
-
{ field: "api_budget", id: "api", label: "API monthly" },
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const MONTH_SECONDS = 2_592_000;
|
|
33
|
-
const PAYG = "pay-as-you-go";
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @param {unknown} value
|
|
37
|
-
* @returns {Record<string, unknown> | null}
|
|
38
|
-
*/
|
|
39
|
-
function record(value) {
|
|
40
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
41
|
-
? /** @type {Record<string, unknown>} */ (value)
|
|
42
|
-
: null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* The account label shown by every surface: the base auth.json entry has no
|
|
47
|
-
* numeric suffix, so it gets no number; a sibling slot is `#<slot>`.
|
|
48
|
-
*
|
|
49
|
-
* @param {{ slot?: string | null }} credential
|
|
50
|
-
* @returns {string}
|
|
51
|
-
*/
|
|
52
|
-
function accountLabel(credential) {
|
|
53
|
-
return credential.slot ? `#${credential.slot}` : "";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* "127.50 EUR", or null when the budget carries no usable amount.
|
|
58
|
-
*
|
|
59
|
-
* @param {unknown} amount
|
|
60
|
-
* @param {unknown} currency
|
|
61
|
-
* @returns {string | null}
|
|
62
|
-
*/
|
|
63
|
-
function money(amount, currency) {
|
|
64
|
-
const value = typeof amount === "number" ? amount : typeof amount === "string" && amount.trim() !== "" ? Number(amount) : NaN;
|
|
65
|
-
// A budget is a few hundred euros; anything absurd is not printed as 1e+308.
|
|
66
|
-
if (!Number.isFinite(value) || value < 0 || value >= 1e9) return null;
|
|
67
|
-
const code = stringValue(currency);
|
|
68
|
-
const safeCode = code && /^[A-Za-z]{3}$/.test(code) ? ` ${code.toUpperCase()}` : "";
|
|
69
|
-
return `${value.toFixed(2)}${safeCode}`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Map a `/billing/budget` body onto canonical windows.
|
|
74
|
-
*
|
|
75
|
-
* Mistral also repeats one budget at the top level of the body (today the API
|
|
76
|
-
* one); it is read only when neither named budget is present, so an older or
|
|
77
|
-
* smaller answer still shows something and a full answer never shows a budget
|
|
78
|
-
* twice.
|
|
79
|
-
*
|
|
80
|
-
* A budget that does not say whether pay-as-you-go is on falls back to the
|
|
81
|
-
* subscription's flag for it (`vibe_payg_enabled` for Vibe, `payg_enabled`
|
|
82
|
-
* for the API).
|
|
83
|
-
*
|
|
84
|
-
* @param {unknown} body
|
|
85
|
-
* @param {number} now
|
|
86
|
-
* @param {{ vibe?: boolean | null, api?: boolean | null }} [payg] From `/billing/subscription`.
|
|
87
|
-
* @returns {import("../model.js").QuotaWindow[]}
|
|
88
|
-
*/
|
|
89
|
-
export function parseBudgetWindows(body, now = Date.now(), payg = {}) {
|
|
90
|
-
const root = record(body);
|
|
91
|
-
if (!root) return [];
|
|
92
|
-
|
|
93
|
-
/** @type {Array<{ id: string, label: string, entry: Record<string, unknown> }>} */
|
|
94
|
-
const named = [];
|
|
95
|
-
for (const spec of BUDGETS) {
|
|
96
|
-
const entry = record(root[spec.field]);
|
|
97
|
-
if (entry) named.push({ id: spec.id, label: spec.label, entry });
|
|
98
|
-
}
|
|
99
|
-
const windows = toWindows(named, now, payg);
|
|
100
|
-
// The top-level copy stands in only when no named budget produced a window,
|
|
101
|
-
// so a full answer never shows a budget twice and an unreadable named one
|
|
102
|
-
// does not hide a readable top-level one.
|
|
103
|
-
if (windows.length === 0 && root.usage_percentage !== undefined) {
|
|
104
|
-
return toWindows([{ id: "budget", label: "Monthly budget", entry: root }], now, payg);
|
|
105
|
-
}
|
|
106
|
-
return windows;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* @param {Array<{ id: string, label: string, entry: Record<string, unknown> }>} budgets
|
|
111
|
-
* @param {number} now
|
|
112
|
-
* @param {{ vibe?: boolean | null, api?: boolean | null }} payg
|
|
113
|
-
* @returns {import("../model.js").QuotaWindow[]}
|
|
114
|
-
*/
|
|
115
|
-
function toWindows(budgets, now, payg) {
|
|
116
|
-
/** @type {import("../model.js").QuotaWindow[]} */
|
|
117
|
-
const windows = [];
|
|
118
|
-
for (const { id, label, entry } of budgets) {
|
|
119
|
-
// No readable percentage, no window: "n/a" must not become an empty bar.
|
|
120
|
-
if (clampPercent(entry.usage_percentage) === null) continue;
|
|
121
|
-
const notes = [];
|
|
122
|
-
const amount = money(entry.initial_budget, entry.currency);
|
|
123
|
-
if (amount) notes.push(`of ${amount}`);
|
|
124
|
-
const paygOn = typeof entry.payg_enabled === "boolean" ? entry.payg_enabled : (id === "vibe" ? payg.vibe : payg.api) === true;
|
|
125
|
-
if (paygOn) notes.push(PAYG);
|
|
126
|
-
windows.push(
|
|
127
|
-
buildWindow({
|
|
128
|
-
id,
|
|
129
|
-
label,
|
|
130
|
-
usedPercent: entry.usage_percentage,
|
|
131
|
-
resetsAt: entry.reset_at ?? null,
|
|
132
|
-
windowSeconds: MONTH_SECONDS,
|
|
133
|
-
note: notes.length > 0 ? notes.join(" · ") : null,
|
|
134
|
-
now,
|
|
135
|
-
}),
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
return windows;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* The pay-as-you-go flags of a `/billing/subscription` body.
|
|
143
|
-
*
|
|
144
|
-
* @param {unknown} body
|
|
145
|
-
* @returns {{ vibe: boolean | null, api: boolean | null }}
|
|
146
|
-
*/
|
|
147
|
-
export function parsePayg(body) {
|
|
148
|
-
const root = record(body);
|
|
149
|
-
const flag = (value) => (typeof value === "boolean" ? value : null);
|
|
150
|
-
return { vibe: flag(root?.vibe_payg_enabled), api: flag(root?.payg_enabled) };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* The plan's name from a `/billing/subscription` body, or null.
|
|
155
|
-
*
|
|
156
|
-
* @param {unknown} body
|
|
157
|
-
* @returns {string | null}
|
|
158
|
-
*/
|
|
159
|
-
export function parsePlanName(body) {
|
|
160
|
-
const name = stringValue(record(body)?.plan_name);
|
|
161
|
-
if (!name) return null;
|
|
162
|
-
// A display string only: keep it short and free of control characters.
|
|
163
|
-
const clean = name.replace(/[\u0000-\u001f\u007f-\u009f]/g, "").trim().slice(0, 40);
|
|
164
|
-
return clean === "" ? null : clean;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* @param {import("../http.js").HttpResult} response
|
|
169
|
-
* @param {string} what
|
|
170
|
-
* @returns {string}
|
|
171
|
-
*/
|
|
172
|
-
function failureMessage(response, what) {
|
|
173
|
-
if (response.status === 401) return "Mistral key rejected (401); run /login mistral in Pi";
|
|
174
|
-
if (response.status === 403) return `Mistral ${what} not readable with this key (403)`;
|
|
175
|
-
if (response.status === 404) return `Mistral billing API not served (404 on ${what})`;
|
|
176
|
-
return `Mistral ${what} request failed: ${response.error ?? `HTTP ${response.status}`}`;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* @param {import("../auth/pi-auth.js").PiCredential} credential
|
|
181
|
-
* @param {{
|
|
182
|
-
* now?: number,
|
|
183
|
-
* fetchFn?: typeof fetch,
|
|
184
|
-
* timeoutMs?: number,
|
|
185
|
-
* expiresInMin?: number | null,
|
|
186
|
-
* }} [options]
|
|
187
|
-
* @returns {Promise<import("../model.js").QuotaResult>}
|
|
188
|
-
*/
|
|
189
|
-
export async function fetchQuota(credential, options = {}) {
|
|
190
|
-
const now = options.now ?? Date.now();
|
|
191
|
-
const base = {
|
|
192
|
-
family: "mistral",
|
|
193
|
-
label: "Mistral (Pi)",
|
|
194
|
-
account: accountLabel(credential),
|
|
195
|
-
source: credential.source,
|
|
196
|
-
sourceKind: credential.sourceKind ?? "pi",
|
|
197
|
-
expiresInMin: options.expiresInMin ?? null,
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
if (!credential.key) {
|
|
201
|
-
return degradedResult({ ...base, error: "no key stored for mistral in the Pi store; run /login mistral in Pi", now });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const request = (url) =>
|
|
205
|
-
requestJson(url, {
|
|
206
|
-
headers: { Authorization: `Bearer ${credential.key}`, Accept: "application/json" },
|
|
207
|
-
fetchFn: options.fetchFn,
|
|
208
|
-
timeoutMs: options.timeoutMs,
|
|
209
|
-
});
|
|
210
|
-
const [subscription, budget] = await Promise.all([request(SUBSCRIPTION_URL), request(BUDGET_URL)]);
|
|
211
|
-
|
|
212
|
-
const plan = subscription.ok ? parsePlanName(subscription.body) : null;
|
|
213
|
-
const payg = subscription.ok ? parsePayg(subscription.body) : { vibe: null, api: null };
|
|
214
|
-
|
|
215
|
-
// A rejected key is a statement about the credential: report it once, as an
|
|
216
|
-
// auth failure, whichever read noticed it.
|
|
217
|
-
if (subscription.status === 401 || budget.status === 401) {
|
|
218
|
-
return degradedResult({ ...base, plan, error: failureMessage({ status: 401 }, "billing"), now });
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (budget.ok) {
|
|
222
|
-
const windows = parseBudgetWindows(budget.body, now, payg);
|
|
223
|
-
if (windows.length === 0) {
|
|
224
|
-
return degradedResult({ ...base, plan, error: "Mistral budget endpoint returned no budgets", now });
|
|
225
|
-
}
|
|
226
|
-
return {
|
|
227
|
-
...base,
|
|
228
|
-
plan,
|
|
229
|
-
windows,
|
|
230
|
-
error: null,
|
|
231
|
-
ok: true,
|
|
232
|
-
updatedAt: new Date(now).toISOString(),
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Without the usage permission the plan is still worth showing: the account
|
|
237
|
-
// works, it just cannot see the organisation's budget.
|
|
238
|
-
if (budget.status === 403 && subscription.ok) {
|
|
239
|
-
return {
|
|
240
|
-
...base,
|
|
241
|
-
plan,
|
|
242
|
-
windows: [],
|
|
243
|
-
note: `budget needs the see_org_inference_usage permission${payg.vibe === true || payg.api === true ? ` · ${PAYG} on` : ""}`,
|
|
244
|
-
error: null,
|
|
245
|
-
ok: true,
|
|
246
|
-
updatedAt: new Date(now).toISOString(),
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return degradedResult({ ...base, plan, error: failureMessage(budget, "budget"), now });
|
|
251
|
-
}
|