@narumitw/pi-usage 0.60.10 → 0.61.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/README.md +20 -5
- package/dist/index.ts +137 -23
- package/dist/index.ts.map +2 -2
- package/docs/providers.md +11 -6
- package/package.json +1 -1
- package/src/codex-fast.ts +1 -0
- package/src/codex-resets.ts +7 -4
- package/src/format.ts +22 -6
- package/src/index.ts +1 -0
- package/src/oauth-credential-source.ts +39 -2
- package/src/providers/kimi-coding.ts +41 -2
- package/src/query.ts +9 -2
- package/src/settings.ts +13 -0
- package/src/types.ts +2 -0
- package/src/usage-settings-ui.ts +27 -7
- package/src/usage.ts +10 -3
package/docs/providers.md
CHANGED
|
@@ -28,9 +28,10 @@ The README contains the capability overview and shared security requirements.
|
|
|
28
28
|
- Source: the Codex usage and earned-reset endpoints using Pi's resolved runtime authorization
|
|
29
29
|
- Displayed data: returned duration-based windows, resets, credits, earned usage-limit resets, and additional model buckets
|
|
30
30
|
- Reset mutation: `POST /wham/rate-limit-reset-credits/consume` with a unique redemption request ID and, when available, the selected opaque credit ID
|
|
31
|
-
- Statusline examples: `codex 59% ↻ 2h30m 61% ↻ 2d15m`, `codex fast 59% ↻ 2h30m`, or `codex spark 100% ↻ 2h30m`. Set `codexStatusResetCountdown` to `false` for the legacy `5h` and `wk` labels.
|
|
31
|
+
- Statusline examples: `codex 59% ↻ 2h30m 61% ↻ 2d15m`, `codex fast 59% ↻ 2h30m`, or `codex spark 100% ↻ 2h30m`. Set `codexStatusPercentage` to `"used"` to show consumed percentages, or set `codexStatusResetCountdown` to `false` for the legacy `5h` and `wk` labels.
|
|
32
32
|
|
|
33
33
|
The statusline selects a returned bucket that matches the current Codex model when one is available.
|
|
34
|
+
The percentage preference affects only this compact status and defaults to remaining quota; it does not change the detailed report or returned usage data.
|
|
34
35
|
Unlike `pi-codex-usage`, this successor intentionally has no Codex CLI fallback because the CLI may be logged into a different account than Pi's active runtime account.
|
|
35
36
|
|
|
36
37
|
Reset redemption is available only when Codex is the current provider.
|
|
@@ -45,17 +46,21 @@ Opaque credit and account IDs are never shown or persisted by the extension.
|
|
|
45
46
|
- Provider ID: `kimi-coding`
|
|
46
47
|
- Semantics: Kimi Coding Plan request windows plus a separate Extra Usage booster wallet
|
|
47
48
|
- Source: `GET https://api.kimi.com/coding/v1/usages` using Pi's freshly resolved runtime Bearer credential
|
|
48
|
-
- Displayed plan data:
|
|
49
|
-
- Displayed wallet data: balance, monthly spend, and monthly charge limit
|
|
50
|
-
- Statusline examples: `kimi 99% 5h 96% wk` or `kimi 95% 1d`
|
|
49
|
+
- Displayed plan data: count-based weekly summaries and returned sub-windows, plus reported ratio-based five-hour, weekly, or monthly quotas and valid reset times
|
|
50
|
+
- Displayed wallet data: balance, monthly spend, and monthly charge limit when the wallet supplies consistent currency and amounts
|
|
51
|
+
- Statusline examples: `kimi 99% 5h 96% wk`, `kimi 56% 5h 21% mo`, or `kimi 95% 1d`
|
|
51
52
|
|
|
52
53
|
Both Pi API-key credentials and Pi OAuth credentials are accepted because current Pi resolves each form as Bearer authorization for the same official Kimi inference origin.
|
|
53
54
|
The extension queries the fixed usage endpoint only when both the selected model origin and the effective resolved-auth origin are `https://api.kimi.com`.
|
|
54
55
|
Custom and proxy origins fail before network access, redirects are rejected, and the credential is never sent to an override from Kimi Code's environment-specific development path.
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
Count-based plan buckets remain integer request counts and are rendered with their source-defined windows.
|
|
57
58
|
Live responses omit both `used` and `remaining` on untouched rows and may report only `remaining` once usage starts, so a row with a positive `limit` and no counters means zero usage, and when only one counter is present the other is derived from `limit`.
|
|
58
|
-
|
|
59
|
+
A monthly-plan response reported in [issue #1390](https://github.com/narumiruna/pi-extensions/issues/1390) supplies `usages.limit_month_total.used_ratio` without a count-based monthly row; the extension shows its remaining percentage and `reset_time` as a monthly bucket without inventing a fixed month duration.
|
|
60
|
+
The same map can supply `limit_5h` and `limit_week` when those windows are not present in `limits` or `usage`; `limit_month_code` is not a second total quota.
|
|
61
|
+
Explicit count-based windows with identifiable durations take precedence, including duplicate or malformed count rows, so ratio rows cannot mask their unavailable state.
|
|
62
|
+
Ratios must be finite numbers between zero and one; unknown map keys, malformed rows, invalid timestamps, and unknown count-based units do not receive guessed semantics.
|
|
63
|
+
The top-level wallet may be `boosterWallet` or `booster_wallet` (the former takes precedence when both are present); its nested fields retain their existing camelCase validation.
|
|
59
64
|
Booster-wallet `amount` and `amountLeft` values use Kimi's first-party conversion of 1,000,000 fixed-point units per cent, while monthly values already arrive in cents.
|
|
60
65
|
Wallet values retain their currency and stay separate from plan requests and percentages in reports and the statusline.
|
|
61
66
|
Wallet fields remain unavailable unless the response supplies one consistent currency; missing monthly values are omitted, and an enabled zero cap is shown as zero.
|
package/package.json
CHANGED
package/src/codex-fast.ts
CHANGED
package/src/codex-resets.ts
CHANGED
|
@@ -101,11 +101,18 @@ export async function resolveCodexResetAuth(
|
|
|
101
101
|
const expectedModel = `${model.provider}/${model.id}`;
|
|
102
102
|
const adapter = adapterForProvider("openai-codex");
|
|
103
103
|
if (!adapter) throw new Error("OpenAI Codex usage support is unavailable.");
|
|
104
|
+
if (candidateReader?.waitUntilReady && !(await candidateReader.waitUntilReady(ctx, "openai-codex"))) {
|
|
105
|
+
throw new Error("OpenAI Codex OAuth credential readiness failed closed.");
|
|
106
|
+
}
|
|
104
107
|
const auth = await resolveUsageAuth(ctx, adapter, salt, credentialReader);
|
|
105
108
|
if (`${ctx.model?.provider}/${ctx.model?.id}` !== expectedModel) {
|
|
106
109
|
throw new Error("The current model changed while resolving Codex reset authentication.");
|
|
107
110
|
}
|
|
108
111
|
if (!auth) throw new Error("No runtime credential is configured for OpenAI Codex.");
|
|
112
|
+
const offered = candidateReader
|
|
113
|
+
? await candidateReader(ctx, "openai-codex")
|
|
114
|
+
: fallbackOAuthCredentialCandidates("openai-codex", credentialReader);
|
|
115
|
+
if (!offered.ok) throw new Error("OpenAI Codex OAuth credential discovery failed closed.");
|
|
109
116
|
|
|
110
117
|
const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
|
|
111
118
|
if (!resolvedAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
|
|
@@ -113,10 +120,6 @@ export async function resolveCodexResetAuth(
|
|
|
113
120
|
if (!resolvedAccountId) {
|
|
114
121
|
throw new Error("The active OpenAI Codex access token did not contain a valid account ID.");
|
|
115
122
|
}
|
|
116
|
-
const offered = candidateReader
|
|
117
|
-
? candidateReader(ctx, "openai-codex")
|
|
118
|
-
: fallbackOAuthCredentialCandidates("openai-codex", credentialReader);
|
|
119
|
-
if (!offered.ok) throw new Error("OpenAI Codex OAuth credential discovery failed closed.");
|
|
120
123
|
const { accountId, storedAccess } = selectCodexResetCredential(
|
|
121
124
|
offered.candidates,
|
|
122
125
|
resolvedAccess,
|
package/src/format.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CodexStatusPercentage } from "./settings.js";
|
|
1
2
|
import type { ProviderUsageState, UsageBucket, UsageDisplayState, UsageModel, UsageReport } from "./types.js";
|
|
2
3
|
|
|
3
4
|
const BAR_SEGMENTS = 20;
|
|
@@ -54,10 +55,11 @@ export function formatUsageStatusline(
|
|
|
54
55
|
model?: UsageModel,
|
|
55
56
|
now = Date.now(),
|
|
56
57
|
showCodexResetCountdown = true,
|
|
58
|
+
codexStatusPercentage: CodexStatusPercentage = "remaining",
|
|
57
59
|
): string | undefined {
|
|
58
60
|
if (report.providerId === "baseten") return formatBasetenStatusline(report);
|
|
59
61
|
if (report.providerId === "openai-codex") {
|
|
60
|
-
return formatCodexStatusline(report, model, now, showCodexResetCountdown);
|
|
62
|
+
return formatCodexStatusline(report, model, now, showCodexResetCountdown, codexStatusPercentage);
|
|
61
63
|
}
|
|
62
64
|
if (report.providerId === "deepseek") return formatDeepSeekStatusline(report);
|
|
63
65
|
if (report.providerId === "fireworks") return formatFireworksStatusline(report);
|
|
@@ -280,6 +282,15 @@ function formatOpenCodeZenStatusline(report: UsageReport): string | undefined {
|
|
|
280
282
|
function formatKimiCodingReport(lines: string[], report: UsageReport): void {
|
|
281
283
|
for (const bucket of report.buckets) {
|
|
282
284
|
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
285
|
+
if (bucket.unit === "percent") {
|
|
286
|
+
const remaining = bucket.remaining === undefined ? undefined : Math.round(clampPercent(bucket.remaining));
|
|
287
|
+
const value =
|
|
288
|
+
bucket.used === undefined || remaining === undefined
|
|
289
|
+
? "unavailable"
|
|
290
|
+
: `${100 - remaining}% used · ${remaining}% left`;
|
|
291
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}${reset}`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
283
294
|
if (bucket.used === undefined || bucket.limit === undefined) {
|
|
284
295
|
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}unavailable${reset}`);
|
|
285
296
|
continue;
|
|
@@ -309,15 +320,18 @@ function formatKimiCodingReport(lines: string[], report: UsageReport): void {
|
|
|
309
320
|
function formatKimiCodingStatusline(report: UsageReport): string | undefined {
|
|
310
321
|
const fiveHour = report.buckets.find((bucket) => bucket.id === "five-hour");
|
|
311
322
|
const weekly = report.buckets.find((bucket) => bucket.id === "weekly");
|
|
312
|
-
const
|
|
313
|
-
const
|
|
323
|
+
const monthly = report.buckets.find((bucket) => bucket.id === "monthly");
|
|
324
|
+
const subWindow = fiveHour ?? report.buckets.find((bucket) => bucket.id !== "weekly" && bucket.id !== "monthly");
|
|
325
|
+
const selected = [subWindow, weekly, monthly].filter(
|
|
314
326
|
(bucket, index, buckets): bucket is UsageBucket => bucket !== undefined && buckets.indexOf(bucket) === index,
|
|
315
327
|
);
|
|
316
328
|
const parts = ["kimi"];
|
|
317
329
|
for (const bucket of selected) {
|
|
318
|
-
if (
|
|
330
|
+
if (bucket.remaining === undefined || (bucket.unit !== "percent" && !bucket.limit)) continue;
|
|
319
331
|
const fallback = bucket.id === "weekly" ? "weekly" : "5h";
|
|
320
|
-
|
|
332
|
+
const window = bucket.id === "monthly" ? "mo" : formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
333
|
+
const remaining = bucket.unit === "percent" ? Math.round(clampPercent(bucket.remaining)) : percentRemaining(bucket);
|
|
334
|
+
parts.push(`${remaining}% ${window}`);
|
|
321
335
|
}
|
|
322
336
|
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
323
337
|
}
|
|
@@ -521,6 +535,7 @@ function formatCodexStatusline(
|
|
|
521
535
|
model?: UsageModel,
|
|
522
536
|
now = Date.now(),
|
|
523
537
|
showResetCountdown = true,
|
|
538
|
+
percentage: CodexStatusPercentage = "remaining",
|
|
524
539
|
): string | undefined {
|
|
525
540
|
const group = selectCodexGroup(report, model);
|
|
526
541
|
if (!group) return formatCodexCreditsStatus(report);
|
|
@@ -529,7 +544,8 @@ function formatCodexStatusline(
|
|
|
529
544
|
const parts = [group === "codex" ? "codex" : `codex ${compactLimitLabel(labelBucket?.groupLabel ?? group)}`];
|
|
530
545
|
for (const bucket of buckets) {
|
|
531
546
|
if (bucket.remaining === undefined) continue;
|
|
532
|
-
const
|
|
547
|
+
const displayedPercentage = percentage === "used" ? (bucket.used ?? 100 - bucket.remaining) : bucket.remaining;
|
|
548
|
+
const percent = `${clampPercent(displayedPercentage).toFixed(0)}%`;
|
|
533
549
|
const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
|
|
534
550
|
const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
535
551
|
if (!showResetCountdown) {
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { OAuthCredential } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import { type ExtensionAPI, type ExtensionContext, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
4
|
export const OAUTH_CREDENTIAL_SOURCE_CHANNEL = "oauth:credential-source:v1";
|
|
5
|
+
export const OAUTH_CREDENTIAL_READINESS_CHANNEL = "oauth:credential-readiness:v1";
|
|
5
6
|
|
|
6
7
|
export type StoredCredentialReader = (providerId: string) => unknown;
|
|
7
8
|
|
|
@@ -9,13 +10,19 @@ export type OAuthCredentialCandidates =
|
|
|
9
10
|
| { ok: true; candidates: readonly OAuthCredential[]; offeredCount?: number }
|
|
10
11
|
| { ok: false };
|
|
11
12
|
|
|
12
|
-
export type OAuthCredentialCandidateReader =
|
|
13
|
+
export type OAuthCredentialCandidateReader = {
|
|
14
|
+
(ctx: ExtensionContext, providerId: string): OAuthCredentialCandidates | Promise<OAuthCredentialCandidates>;
|
|
15
|
+
waitUntilReady?(ctx: ExtensionContext, providerId: string): Promise<boolean>;
|
|
16
|
+
};
|
|
13
17
|
|
|
14
18
|
export function createOAuthCredentialCandidateReader(
|
|
15
19
|
pi: ExtensionAPI,
|
|
16
20
|
credentialReader: StoredCredentialReader = readStoredCredential,
|
|
17
21
|
): OAuthCredentialCandidateReader {
|
|
18
|
-
|
|
22
|
+
const reader: OAuthCredentialCandidateReader = (ctx, providerId) =>
|
|
23
|
+
collectOAuthCredentialCandidates(pi, ctx, providerId, credentialReader);
|
|
24
|
+
reader.waitUntilReady = (ctx, providerId) => waitForOAuthCredentialSources(pi, ctx, providerId);
|
|
25
|
+
return reader;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
export function collectOAuthCredentialCandidates(
|
|
@@ -52,6 +59,36 @@ export function collectOAuthCredentialCandidates(
|
|
|
52
59
|
return { ok: true, candidates, offeredCount };
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
async function waitForOAuthCredentialSources(
|
|
63
|
+
pi: Pick<ExtensionAPI, "events">,
|
|
64
|
+
ctx: ExtensionContext,
|
|
65
|
+
providerId: string,
|
|
66
|
+
): Promise<boolean> {
|
|
67
|
+
const pending: Promise<unknown>[] = [];
|
|
68
|
+
let collecting = true;
|
|
69
|
+
const request = Object.freeze({
|
|
70
|
+
session: ctx.sessionManager,
|
|
71
|
+
provider: providerId,
|
|
72
|
+
waitUntil(value: unknown) {
|
|
73
|
+
if (!collecting || !(value instanceof Promise)) return;
|
|
74
|
+
pending.push(value);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
pi.events.emit(OAUTH_CREDENTIAL_READINESS_CHANNEL, request);
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
} finally {
|
|
82
|
+
collecting = false;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await Promise.all(pending);
|
|
86
|
+
return true;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
export function fallbackOAuthCredentialCandidates(
|
|
56
93
|
providerId: string,
|
|
57
94
|
credentialReader: StoredCredentialReader,
|
|
@@ -5,6 +5,11 @@ const FIVE_HOUR_WINDOW_MINUTES = 300;
|
|
|
5
5
|
const DAILY_WINDOW_MINUTES = 1_440;
|
|
6
6
|
const WEEKLY_WINDOW_MINUTES = 10_080;
|
|
7
7
|
const FIXED_POINT_UNITS_PER_CENT = 1_000_000;
|
|
8
|
+
const RATIO_WINDOWS = [
|
|
9
|
+
{ key: "limit_5h", id: "five-hour", label: "Five-hour window", windowMinutes: FIVE_HOUR_WINDOW_MINUTES },
|
|
10
|
+
{ key: "limit_week", id: "weekly", label: "Weekly window", windowMinutes: WEEKLY_WINDOW_MINUTES },
|
|
11
|
+
{ key: "limit_month_total", id: "monthly", label: "Monthly window" },
|
|
12
|
+
] as const;
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
15
|
* Source contract revalidated on 2026-08-27.
|
|
@@ -24,7 +29,9 @@ export function normalizeKimiCodingUsagePayload(payload: KimiCodingUsagePayload,
|
|
|
24
29
|
if (!root) throw new Error("Kimi Coding usage response was not an object.");
|
|
25
30
|
|
|
26
31
|
const candidates: UsageBucket[] = [];
|
|
32
|
+
const explicitWindows = new Set<number>();
|
|
27
33
|
let omittedWindow = false;
|
|
34
|
+
if (root.usage !== undefined) explicitWindows.add(WEEKLY_WINDOW_MINUTES);
|
|
28
35
|
const summary = parseUsageRow(root.usage, WEEKLY_WINDOW_MINUTES, "Weekly window");
|
|
29
36
|
if (summary) candidates.push(summary);
|
|
30
37
|
else if (root.usage !== undefined) omittedWindow = true;
|
|
@@ -33,6 +40,7 @@ export function normalizeKimiCodingUsagePayload(payload: KimiCodingUsagePayload,
|
|
|
33
40
|
for (const raw of root.limits) {
|
|
34
41
|
const item = asObject(raw);
|
|
35
42
|
const windowMinutes = parseWindowMinutes(item?.window);
|
|
43
|
+
if (windowMinutes !== undefined) explicitWindows.add(windowMinutes);
|
|
36
44
|
const label = sanitizedLabel(item?.name);
|
|
37
45
|
const bucket =
|
|
38
46
|
windowMinutes === undefined
|
|
@@ -55,9 +63,24 @@ export function normalizeKimiCodingUsagePayload(payload: KimiCodingUsagePayload,
|
|
|
55
63
|
if (rows.length === 1) buckets.push(rows[0] as UsageBucket);
|
|
56
64
|
else omittedWindow = true;
|
|
57
65
|
}
|
|
58
|
-
|
|
66
|
+
const usages = asObject(root.usages);
|
|
67
|
+
if (root.usages !== undefined && !usages) omittedWindow = true;
|
|
68
|
+
if (usages) {
|
|
69
|
+
for (const window of RATIO_WINDOWS) {
|
|
70
|
+
const raw = usages[window.key];
|
|
71
|
+
if (raw === undefined || ("windowMinutes" in window && explicitWindows.has(window.windowMinutes))) continue;
|
|
72
|
+
const bucket = parseRatioWindow(raw, window.id, window.label);
|
|
73
|
+
if (bucket) {
|
|
74
|
+
buckets.push({
|
|
75
|
+
...bucket,
|
|
76
|
+
...("windowMinutes" in window ? { windowMinutes: window.windowMinutes } : {}),
|
|
77
|
+
});
|
|
78
|
+
} else omittedWindow = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
buckets.sort((left, right) => (left.windowMinutes ?? Infinity) - (right.windowMinutes ?? Infinity));
|
|
59
82
|
|
|
60
|
-
const metrics = parseBoosterWallet(root.boosterWallet);
|
|
83
|
+
const metrics = parseBoosterWallet(root.boosterWallet === undefined ? root.booster_wallet : root.boosterWallet);
|
|
61
84
|
if (buckets.length === 0 && metrics.length === 0) {
|
|
62
85
|
throw new Error("Kimi Coding usage endpoint returned no displayable usage data.");
|
|
63
86
|
}
|
|
@@ -104,6 +127,22 @@ function parseUsageRow(value: unknown, windowMinutes: number, label: string): Us
|
|
|
104
127
|
};
|
|
105
128
|
}
|
|
106
129
|
|
|
130
|
+
function parseRatioWindow(value: unknown, id: string, label: string): UsageBucket | undefined {
|
|
131
|
+
const row = asObject(value);
|
|
132
|
+
const ratio = row?.used_ratio;
|
|
133
|
+
if (typeof ratio !== "number" || !Number.isFinite(ratio) || ratio < 0 || ratio > 1) return undefined;
|
|
134
|
+
const resetsAt = asIsoEpochSeconds(row?.reset_time);
|
|
135
|
+
const used = ratio * 100;
|
|
136
|
+
return {
|
|
137
|
+
id,
|
|
138
|
+
label,
|
|
139
|
+
used,
|
|
140
|
+
remaining: 100 - used,
|
|
141
|
+
unit: "percent",
|
|
142
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
107
146
|
function parseWindowMinutes(value: unknown): number | undefined {
|
|
108
147
|
const window = asObject(value);
|
|
109
148
|
if (!window) return undefined;
|
package/src/query.ts
CHANGED
|
@@ -343,6 +343,13 @@ export async function resolveUsageAuth(
|
|
|
343
343
|
|
|
344
344
|
const model = candidateModels(ctx, adapter.id).find((candidate) => hasOfficialOrigin(candidate, adapter.id));
|
|
345
345
|
if (!model) return undefined;
|
|
346
|
+
if (
|
|
347
|
+
["github-copilot", "xai"].includes(adapter.id) &&
|
|
348
|
+
candidateReader?.waitUntilReady &&
|
|
349
|
+
!(await candidateReader.waitUntilReady(ctx, adapter.id))
|
|
350
|
+
) {
|
|
351
|
+
throw new Error(`${adapter.displayName} OAuth credential readiness failed closed.`);
|
|
352
|
+
}
|
|
346
353
|
// SAFETY: Pi exposes the required auth methods at runtime, and checks below narrow them before use.
|
|
347
354
|
const registry = ctx.modelRegistry as unknown as UsageAuthRegistry;
|
|
348
355
|
const provider = registry.getProvider?.(adapter.id);
|
|
@@ -416,7 +423,7 @@ export async function resolveUsageAuth(
|
|
|
416
423
|
};
|
|
417
424
|
if (adapter.id === "github-copilot") {
|
|
418
425
|
const offered = candidateReader
|
|
419
|
-
? candidateReader(ctx, adapter.id)
|
|
426
|
+
? await candidateReader(ctx, adapter.id)
|
|
420
427
|
: fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
|
|
421
428
|
if (!offered.ok) {
|
|
422
429
|
throw new Error("GitHub Copilot OAuth credential discovery failed closed.");
|
|
@@ -425,7 +432,7 @@ export async function resolveUsageAuth(
|
|
|
425
432
|
}
|
|
426
433
|
if (adapter.id === "xai") {
|
|
427
434
|
const offered = candidateReader
|
|
428
|
-
? candidateReader(ctx, adapter.id)
|
|
435
|
+
? await candidateReader(ctx, adapter.id)
|
|
429
436
|
: fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
|
|
430
437
|
if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
|
|
431
438
|
return finalize(resolveXaiUsageAuth(auth, model, salt, offered.candidates));
|
package/src/settings.ts
CHANGED
|
@@ -9,15 +9,19 @@ import { isBoundedTargetId } from "./usage-targets.js";
|
|
|
9
9
|
export const USAGE_SETTINGS_FILE = "pi-usage.json";
|
|
10
10
|
export const MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
|
|
11
11
|
|
|
12
|
+
export type CodexStatusPercentage = "remaining" | "used";
|
|
13
|
+
|
|
12
14
|
export interface UsageSettings {
|
|
13
15
|
codexFastMode: boolean;
|
|
14
16
|
codexStatusResetCountdown: boolean;
|
|
17
|
+
codexStatusPercentage: CodexStatusPercentage;
|
|
15
18
|
selectedTargets: Record<string, string>;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export const DEFAULT_USAGE_SETTINGS: Readonly<UsageSettings> = Object.freeze({
|
|
19
22
|
codexFastMode: false,
|
|
20
23
|
codexStatusResetCountdown: true,
|
|
24
|
+
codexStatusPercentage: "remaining",
|
|
21
25
|
selectedTargets: Object.freeze({}),
|
|
22
26
|
});
|
|
23
27
|
|
|
@@ -66,6 +70,13 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
|
|
|
66
70
|
if (Object.hasOwn(value, "codexStatusResetCountdown") && typeof value.codexStatusResetCountdown !== "boolean") {
|
|
67
71
|
return undefined;
|
|
68
72
|
}
|
|
73
|
+
if (
|
|
74
|
+
Object.hasOwn(value, "codexStatusPercentage") &&
|
|
75
|
+
value.codexStatusPercentage !== "remaining" &&
|
|
76
|
+
value.codexStatusPercentage !== "used"
|
|
77
|
+
) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
69
80
|
if (Object.hasOwn(value, "fireworksAccountId") && !isFireworksAccountId(value.fireworksAccountId)) {
|
|
70
81
|
return undefined;
|
|
71
82
|
}
|
|
@@ -82,6 +93,8 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
|
|
|
82
93
|
typeof value.codexStatusResetCountdown === "boolean"
|
|
83
94
|
? value.codexStatusResetCountdown
|
|
84
95
|
: DEFAULT_USAGE_SETTINGS.codexStatusResetCountdown,
|
|
96
|
+
codexStatusPercentage:
|
|
97
|
+
value.codexStatusPercentage === "used" ? "used" : DEFAULT_USAGE_SETTINGS.codexStatusPercentage,
|
|
85
98
|
selectedTargets: effectiveTargets,
|
|
86
99
|
};
|
|
87
100
|
}
|
package/src/types.ts
CHANGED
package/src/usage-settings-ui.ts
CHANGED
|
@@ -2,12 +2,19 @@ import { stripVTControlCharacters } from "node:util";
|
|
|
2
2
|
import { type ExtensionCommandContext, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Key, matchesKey, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { errorMessage } from "./core.js";
|
|
5
|
-
import type { UsageSettingsRuntime } from "./settings.js";
|
|
5
|
+
import type { UsageSettings, UsageSettingsRuntime } from "./settings.js";
|
|
6
6
|
|
|
7
7
|
const OFF = "Off";
|
|
8
8
|
const ON = "On";
|
|
9
|
+
const REMAINING = "Remaining";
|
|
10
|
+
const USED = "Used";
|
|
9
11
|
|
|
10
|
-
type UsageSettingId = "codexFastMode" | "codexStatusResetCountdown";
|
|
12
|
+
type UsageSettingId = "codexFastMode" | "codexStatusResetCountdown" | "codexStatusPercentage";
|
|
13
|
+
|
|
14
|
+
function settingValueLabel(id: UsageSettingId, value: UsageSettings[UsageSettingId]): string {
|
|
15
|
+
if (id === "codexStatusPercentage") return value === "used" ? USED : REMAINING;
|
|
16
|
+
return value === true ? ON : OFF;
|
|
17
|
+
}
|
|
11
18
|
|
|
12
19
|
export async function showUsageSettings(
|
|
13
20
|
ctx: ExtensionCommandContext,
|
|
@@ -45,6 +52,13 @@ export async function showUsageSettings(
|
|
|
45
52
|
currentValue: state.settings.codexStatusResetCountdown ? ON : OFF,
|
|
46
53
|
values: [OFF, ON],
|
|
47
54
|
},
|
|
55
|
+
{
|
|
56
|
+
id: "codexStatusPercentage",
|
|
57
|
+
label: "Codex percentage",
|
|
58
|
+
description: "Show remaining or used Codex quota in the statusline.",
|
|
59
|
+
currentValue: settingValueLabel("codexStatusPercentage", state.settings.codexStatusPercentage),
|
|
60
|
+
values: [REMAINING, USED],
|
|
61
|
+
},
|
|
48
62
|
];
|
|
49
63
|
const rule = new HorizontalRule({ ruleStyle: (text) => theme.fg("border", text) });
|
|
50
64
|
|
|
@@ -55,11 +69,11 @@ export async function showUsageSettings(
|
|
|
55
69
|
localController.abort();
|
|
56
70
|
done(changed);
|
|
57
71
|
};
|
|
58
|
-
const queueUpdate = (id:
|
|
72
|
+
const queueUpdate = <Id extends UsageSettingId>(id: Id, requested: UsageSettings[Id], display: string) => {
|
|
59
73
|
saveQueue = saveQueue.then(async () => {
|
|
60
74
|
const previous = settingsRuntime.get().settings[id];
|
|
61
75
|
if (settingsRuntime.get().kind === "invalid") {
|
|
62
|
-
settingsList.updateValue(id, previous
|
|
76
|
+
settingsList.updateValue(id, settingValueLabel(id, previous));
|
|
63
77
|
if (!signal.aborted && isCurrent()) {
|
|
64
78
|
ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
|
|
65
79
|
tui.requestRender();
|
|
@@ -67,10 +81,12 @@ export async function showUsageSettings(
|
|
|
67
81
|
return;
|
|
68
82
|
}
|
|
69
83
|
try {
|
|
70
|
-
|
|
84
|
+
const patch: Partial<UsageSettings> = {};
|
|
85
|
+
patch[id] = requested;
|
|
86
|
+
await settingsRuntime.update(patch, signal);
|
|
71
87
|
} catch (error) {
|
|
72
88
|
if (signal.aborted || !isCurrent()) return;
|
|
73
|
-
settingsList.updateValue(id, previous
|
|
89
|
+
settingsList.updateValue(id, settingValueLabel(id, previous));
|
|
74
90
|
ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
|
|
75
91
|
tui.requestRender();
|
|
76
92
|
return;
|
|
@@ -90,7 +106,11 @@ export async function showUsageSettings(
|
|
|
90
106
|
getSettingsListTheme(),
|
|
91
107
|
(id, value) => {
|
|
92
108
|
if (closing || signal.aborted || !isCurrent()) return;
|
|
93
|
-
|
|
109
|
+
if (id === "codexStatusPercentage") {
|
|
110
|
+
queueUpdate(id, value === USED ? "used" : "remaining", value);
|
|
111
|
+
} else if (id === "codexFastMode" || id === "codexStatusResetCountdown") {
|
|
112
|
+
queueUpdate(id, value === ON, value);
|
|
113
|
+
}
|
|
94
114
|
},
|
|
95
115
|
cancel,
|
|
96
116
|
);
|
package/src/usage.ts
CHANGED
|
@@ -166,10 +166,17 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
166
166
|
}
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
|
+
const settings = settingsRuntime.get().settings;
|
|
169
170
|
const showCodexResetCountdown =
|
|
170
|
-
outcome.state.report.providerId === "openai-codex" &&
|
|
171
|
+
outcome.state.report.providerId === "openai-codex" && settings.codexStatusResetCountdown;
|
|
171
172
|
const now = Date.now();
|
|
172
|
-
const rawValue = formatUsageStatusline(
|
|
173
|
+
const rawValue = formatUsageStatusline(
|
|
174
|
+
outcome.state.report,
|
|
175
|
+
model,
|
|
176
|
+
now,
|
|
177
|
+
showCodexResetCountdown,
|
|
178
|
+
settings.codexStatusPercentage,
|
|
179
|
+
);
|
|
173
180
|
const value = rawValue ? fastRuntime.decorateStatus(model, rawValue) : undefined;
|
|
174
181
|
if (!safeSetStatus(ctx, value)) return;
|
|
175
182
|
if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
|
|
@@ -998,7 +1005,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
998
1005
|
() => statusGeneration === menuGeneration && !controller.signal.aborted,
|
|
999
1006
|
(id) => {
|
|
1000
1007
|
if (
|
|
1001
|
-
id === "codexStatusResetCountdown" &&
|
|
1008
|
+
(id === "codexStatusResetCountdown" || id === "codexStatusPercentage") &&
|
|
1002
1009
|
stableCurrent &&
|
|
1003
1010
|
statusGeneration === menuGeneration &&
|
|
1004
1011
|
!controller.signal.aborted
|