@narumitw/pi-usage 0.52.2 → 0.53.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 -6
- package/dist/index.ts +212 -17
- package/dist/index.ts.map +4 -4
- package/package.json +5 -3
- package/src/format.ts +27 -1
- package/src/index.ts +1 -0
- package/src/providers/zai.ts +150 -0
- package/src/query.ts +56 -5
- package/src/types.ts +5 -0
- package/src/usage.ts +9 -0
package/README.md
CHANGED
|
@@ -148,11 +148,25 @@ OpenRouter documents the distinction between credit and rate limits in its [API
|
|
|
148
148
|
|
|
149
149
|
- Provider ID: `opencode-go`
|
|
150
150
|
- Semantics: OpenCode Zen plan usage windows—rolling, weekly, and monthly
|
|
151
|
-
- Source: `GET
|
|
151
|
+
- Source: `GET https://opencode.ai/zen/go/v1/usage` using Pi's resolved inference API key
|
|
152
152
|
- Displayed data: used percentage and reset time for each window; `rate-limited` windows remain visible at their reported usage, while unknown statuses are reported as unavailable notes
|
|
153
153
|
- Statusline examples: `zen 0% r 4% w 2% m`
|
|
154
154
|
|
|
155
|
-
The
|
|
155
|
+
The fixed endpoint is queried only when the candidate OpenCode Go model and the resolved provider-auth base URL, when present, use the official `https://opencode.ai` origin; other origins fail before sending the credential.
|
|
156
|
+
|
|
157
|
+
### Z.AI (GLM Coding Plan)
|
|
158
|
+
|
|
159
|
+
- Provider ID: `zai` and `zai-coding-cn`
|
|
160
|
+
- Semantics: GLM Coding Plan quota windows—the rolling 5-hour and weekly plan-usage windows plus the monthly MCP allowance
|
|
161
|
+
- Source: Z.AI's undocumented `GET {origin}/api/monitor/usage/quota/limit` endpoint, also used by its official coding plugin, with the origin derived from the model base URL (`https://api.z.ai` or `https://open.bigmodel.cn`)
|
|
162
|
+
- Displayed data: explicit used and remaining values, reset times, provider-reported per-tool MCP details, and the reported plan level. Windows that report only a percentage remain percent-based
|
|
163
|
+
- Statusline: not published; Z.AI is queried only through `/usage` actions
|
|
164
|
+
|
|
165
|
+
The monitor endpoint is not a published API contract and may return legacy `TOKENS_LIMIT` or newer `CREDIT_LIMIT` window names.
|
|
166
|
+
The extension classifies both forms by the provider's window unit and does not label provider-reported counts as tokens or calls.
|
|
167
|
+
The quota monitor expects the raw API key without a `Bearer` prefix, so the extension strips a `Bearer` prefix from the resolved authorization before sending it to the monitor endpoint.
|
|
168
|
+
Fingerprinting and redaction keep using the original resolved credential.
|
|
169
|
+
Only the official `api.z.ai` and `open.bigmodel.cn` origins are queried; other origins fail before sending the credential.
|
|
156
170
|
|
|
157
171
|
## 🧭 Current and configured accounts
|
|
158
172
|
|
|
@@ -168,8 +182,8 @@ After the active runtime credential changes, the next command, turn, or schedule
|
|
|
168
182
|
|
|
169
183
|
## 📊 Statusline behavior
|
|
170
184
|
|
|
171
|
-
The `usage` status item is active only for
|
|
172
|
-
It refreshes every five minutes while the session remains on a
|
|
185
|
+
The `usage` status item is active only for selected providers that publish statusline usage.
|
|
186
|
+
It refreshes every five minutes while the session remains on such a provider and is cleared when the model changes to an unsupported or menu-only provider.
|
|
173
187
|
|
|
174
188
|
Manual another-provider and all-provider queries never publish to the statusline.
|
|
175
189
|
`@narumitw/pi-statusline` supplies the default `📊` icon; `pi-usage` publishes text-only values.
|
|
@@ -205,7 +219,7 @@ Protocol v1 interoperability is characterized for the repository's supported Pi
|
|
|
205
219
|
## 🚧 Limitations
|
|
206
220
|
|
|
207
221
|
- Only providers with a meaningful usage source and verifiable Pi runtime auth are supported.
|
|
208
|
-
- GitHub Copilot quota and OpenAI Codex reset redemption use undocumented provider endpoints that may change without notice.
|
|
222
|
+
- GitHub Copilot quota, Z.AI quota, and OpenAI Codex reset redemption use undocumented provider endpoints that may change without notice.
|
|
209
223
|
- Codex reset redemption requires a current ChatGPT OAuth credential from Pi's login or a compatible credential source; Codex API keys cannot redeem earned subscription resets.
|
|
210
224
|
- Credentials resolved for custom provider base URLs are never forwarded to the providers' official usage endpoints; effective auth origin validation requires Pi 0.81.0 or newer.
|
|
211
225
|
- Provider reports are snapshots and may themselves be delayed by the provider.
|
|
@@ -235,7 +249,7 @@ packages/pi-usage/
|
|
|
235
249
|
│ ├── codex-resets.ts # Codex reset auth, API contracts, and normalization
|
|
236
250
|
│ ├── format.ts # Provider-aware notifications and statusline text
|
|
237
251
|
│ ├── core.ts # Cache, concurrency, fingerprint, and redaction helpers
|
|
238
|
-
│ ├── providers/ #
|
|
252
|
+
│ ├── providers/ # Provider-specific usage normalization adapters
|
|
239
253
|
│ └── types.ts # Common presentation and adapter contracts
|
|
240
254
|
├── test/
|
|
241
255
|
├── README.md
|
package/dist/index.ts
CHANGED
|
@@ -682,10 +682,125 @@ function asNonnegativeNumber3(value) {
|
|
|
682
682
|
return value;
|
|
683
683
|
}
|
|
684
684
|
|
|
685
|
+
// src/providers/zai.ts
|
|
686
|
+
var FIVE_HOUR_WINDOW_MINUTES = 300;
|
|
687
|
+
var WEEKLY_WINDOW_MINUTES = 10080;
|
|
688
|
+
function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt) {
|
|
689
|
+
const data = asObject5(payload.data);
|
|
690
|
+
if (!data) throw new Error("Z.AI quota response data was not an object.");
|
|
691
|
+
const limits = Array.isArray(data.limits) ? data.limits : [];
|
|
692
|
+
const buckets = [];
|
|
693
|
+
const metrics = [];
|
|
694
|
+
for (const raw of limits) {
|
|
695
|
+
const limit = asObject5(raw);
|
|
696
|
+
if (!limit) continue;
|
|
697
|
+
const type = asString5(limit.type);
|
|
698
|
+
const unit = asNonnegativeNumber4(limit.unit);
|
|
699
|
+
const isPlanUsage = type === "TOKENS_LIMIT" || type === "CREDIT_LIMIT";
|
|
700
|
+
if (type === "TIME_LIMIT") {
|
|
701
|
+
addCountBucket(buckets, limit, "mcp-monthly", "MCP monthly allowance");
|
|
702
|
+
addUsageDetailMetrics(metrics, limit.usageDetails);
|
|
703
|
+
} else if (isPlanUsage && unit === 3) {
|
|
704
|
+
addPercentBucket(buckets, limit, "five-hour", "5h window", FIVE_HOUR_WINDOW_MINUTES);
|
|
705
|
+
} else if (isPlanUsage && unit === 6) {
|
|
706
|
+
const used = asNonnegativeNumber4(limit.currentValue);
|
|
707
|
+
const quota = asNonnegativeNumber4(limit.usage);
|
|
708
|
+
if (used !== void 0 && quota !== void 0) {
|
|
709
|
+
addCountBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES);
|
|
710
|
+
} else {
|
|
711
|
+
addPercentBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (buckets.length === 0) {
|
|
716
|
+
throw new Error("Z.AI quota endpoint returned no displayable usage data.");
|
|
717
|
+
}
|
|
718
|
+
const notes = [];
|
|
719
|
+
const level = asString5(data.level);
|
|
720
|
+
if (level) notes.push(`Plan: ${level}`);
|
|
721
|
+
return {
|
|
722
|
+
providerId,
|
|
723
|
+
providerName,
|
|
724
|
+
capturedAt,
|
|
725
|
+
source: "zai-quota",
|
|
726
|
+
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
727
|
+
buckets,
|
|
728
|
+
metrics,
|
|
729
|
+
...notes.length > 0 ? { notes } : {}
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
function addPercentBucket(buckets, limit, id, label, windowMinutes) {
|
|
733
|
+
const used = asNonnegativeNumber4(limit.percentage);
|
|
734
|
+
if (used === void 0) return;
|
|
735
|
+
const percent = clampPercent3(used);
|
|
736
|
+
const resetsAt = asEpochSeconds2(limit.nextResetTime);
|
|
737
|
+
buckets.push({
|
|
738
|
+
id,
|
|
739
|
+
label,
|
|
740
|
+
used: percent,
|
|
741
|
+
remaining: 100 - percent,
|
|
742
|
+
limit: 100,
|
|
743
|
+
unit: "percent",
|
|
744
|
+
windowMinutes,
|
|
745
|
+
...resetsAt !== void 0 ? { resetsAt } : {}
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
function addCountBucket(buckets, limit, id, label, windowMinutes) {
|
|
749
|
+
const used = asNonnegativeNumber4(limit.currentValue);
|
|
750
|
+
const quota = asNonnegativeNumber4(limit.usage);
|
|
751
|
+
if (used === void 0 || quota === void 0) return;
|
|
752
|
+
const resetsAt = asEpochSeconds2(limit.nextResetTime);
|
|
753
|
+
buckets.push({
|
|
754
|
+
id,
|
|
755
|
+
label,
|
|
756
|
+
used,
|
|
757
|
+
remaining: Math.max(0, quota - used),
|
|
758
|
+
limit: quota,
|
|
759
|
+
unit: "count",
|
|
760
|
+
...windowMinutes !== void 0 ? { windowMinutes } : {},
|
|
761
|
+
...resetsAt !== void 0 ? { resetsAt } : {}
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
function addUsageDetailMetrics(metrics, value) {
|
|
765
|
+
if (!Array.isArray(value)) return;
|
|
766
|
+
for (const raw of value) {
|
|
767
|
+
const detail = asObject5(raw);
|
|
768
|
+
if (!detail) continue;
|
|
769
|
+
const label = asString5(detail.modelCode);
|
|
770
|
+
const usage = asNonnegativeNumber4(detail.usage);
|
|
771
|
+
if (!label || usage === void 0) continue;
|
|
772
|
+
metrics.push({ id: `mcp-${kebabCase(label)}`, label, value: usage, unit: "count" });
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function asObject5(value) {
|
|
776
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
777
|
+
return value;
|
|
778
|
+
}
|
|
779
|
+
function asString5(value) {
|
|
780
|
+
if (typeof value !== "string") return void 0;
|
|
781
|
+
return sanitizeDisplayText(value, 80) || void 0;
|
|
782
|
+
}
|
|
783
|
+
function asNonnegativeNumber4(value) {
|
|
784
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
|
|
785
|
+
return value;
|
|
786
|
+
}
|
|
787
|
+
function asEpochSeconds2(value) {
|
|
788
|
+
const millis = asNonnegativeNumber4(value);
|
|
789
|
+
if (millis === void 0) return void 0;
|
|
790
|
+
return Math.floor(millis / 1e3);
|
|
791
|
+
}
|
|
792
|
+
function kebabCase(label) {
|
|
793
|
+
return label.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "") || "tool";
|
|
794
|
+
}
|
|
795
|
+
function clampPercent3(value) {
|
|
796
|
+
return Math.min(100, Math.max(0, value));
|
|
797
|
+
}
|
|
798
|
+
|
|
685
799
|
// src/query.ts
|
|
686
800
|
var CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
687
801
|
var GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
|
|
688
802
|
var OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
|
|
803
|
+
var OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
689
804
|
var MAX_SUCCESS_BODY_BYTES = 64 * 1024;
|
|
690
805
|
var MAX_ERROR_BODY_BYTES = 4 * 1024;
|
|
691
806
|
var AUTH_FINGERPRINT_SALT = randomBytes(32);
|
|
@@ -747,7 +862,7 @@ var SUPPORTED_ADAPTERS = [
|
|
|
747
862
|
semantics: { kind: "consumer-subscription", label: "OpenCode Zen plan usage" },
|
|
748
863
|
async query(auth, signal, timeoutMs) {
|
|
749
864
|
const payload = await fetchProviderJson(
|
|
750
|
-
|
|
865
|
+
OPENCODE_GO_USAGE_URL,
|
|
751
866
|
auth,
|
|
752
867
|
signal,
|
|
753
868
|
timeoutMs,
|
|
@@ -755,6 +870,43 @@ var SUPPORTED_ADAPTERS = [
|
|
|
755
870
|
);
|
|
756
871
|
return normalizeOpenCodeZenPayload(payload, Date.now());
|
|
757
872
|
}
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
id: "zai",
|
|
876
|
+
displayName: "Z.AI",
|
|
877
|
+
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
878
|
+
publishesStatusline: false,
|
|
879
|
+
async query(auth, signal, timeoutMs) {
|
|
880
|
+
const payload = await fetchProviderJson(
|
|
881
|
+
zaiMonitorUrl(auth.model.baseUrl),
|
|
882
|
+
zaiMonitorAuth(auth),
|
|
883
|
+
signal,
|
|
884
|
+
timeoutMs,
|
|
885
|
+
"Z.AI quota endpoint"
|
|
886
|
+
);
|
|
887
|
+
return normalizeZaiQuotaPayload("zai", "Z.AI", payload, Date.now());
|
|
888
|
+
}
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
id: "zai-coding-cn",
|
|
892
|
+
displayName: "Z.AI Coding CN",
|
|
893
|
+
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
894
|
+
publishesStatusline: false,
|
|
895
|
+
async query(auth, signal, timeoutMs) {
|
|
896
|
+
const payload = await fetchProviderJson(
|
|
897
|
+
zaiMonitorUrl(auth.model.baseUrl),
|
|
898
|
+
zaiMonitorAuth(auth),
|
|
899
|
+
signal,
|
|
900
|
+
timeoutMs,
|
|
901
|
+
"Z.AI Coding CN quota endpoint"
|
|
902
|
+
);
|
|
903
|
+
return normalizeZaiQuotaPayload(
|
|
904
|
+
"zai-coding-cn",
|
|
905
|
+
"Z.AI Coding CN",
|
|
906
|
+
payload,
|
|
907
|
+
Date.now()
|
|
908
|
+
);
|
|
909
|
+
}
|
|
758
910
|
}
|
|
759
911
|
];
|
|
760
912
|
function adapterForProvider(providerId) {
|
|
@@ -953,7 +1105,7 @@ function resolveGitHubCopilotUsageAuth(auth, model, salt, candidates, standalone
|
|
|
953
1105
|
const matches = /* @__PURE__ */ new Map();
|
|
954
1106
|
for (const candidate of candidates) {
|
|
955
1107
|
try {
|
|
956
|
-
const credential =
|
|
1108
|
+
const credential = asObject6(candidate);
|
|
957
1109
|
if (credential?.type !== "oauth") continue;
|
|
958
1110
|
sawOAuth = true;
|
|
959
1111
|
const storedAccess2 = typeof credential.access === "string" && credential.access ? credential.access : void 0;
|
|
@@ -1015,7 +1167,7 @@ function bearerToken(authorization) {
|
|
|
1015
1167
|
const match = /^Bearer\s+(.+)$/iu.exec(authorization ?? "");
|
|
1016
1168
|
return match?.[1];
|
|
1017
1169
|
}
|
|
1018
|
-
function
|
|
1170
|
+
function asObject6(value) {
|
|
1019
1171
|
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1020
1172
|
return value;
|
|
1021
1173
|
}
|
|
@@ -1036,6 +1188,8 @@ function hasOfficialUrlOrigin(value, providerId) {
|
|
|
1036
1188
|
if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
|
|
1037
1189
|
if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
|
|
1038
1190
|
if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
|
|
1191
|
+
if (providerId === "zai") return url.origin === "https://api.z.ai";
|
|
1192
|
+
if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
|
|
1039
1193
|
if (providerId === "github-copilot") {
|
|
1040
1194
|
return url.protocol === "https:" && /^api\.[a-z0-9-]+\.githubcopilot\.com$/u.test(url.hostname);
|
|
1041
1195
|
}
|
|
@@ -1053,10 +1207,16 @@ function headerValue(headers, name) {
|
|
|
1053
1207
|
function hasHeader(headers, name) {
|
|
1054
1208
|
return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
|
|
1055
1209
|
}
|
|
1056
|
-
function
|
|
1057
|
-
const base = baseUrl?.trim()
|
|
1058
|
-
if (!base) throw new Error("
|
|
1059
|
-
return `${base}/usage`;
|
|
1210
|
+
function zaiMonitorUrl(baseUrl) {
|
|
1211
|
+
const base = baseUrl?.trim();
|
|
1212
|
+
if (!base) throw new Error("Z.AI model base URL is unavailable.");
|
|
1213
|
+
return `${new URL(base).origin}/api/monitor/usage/quota/limit`;
|
|
1214
|
+
}
|
|
1215
|
+
function zaiMonitorAuth(auth) {
|
|
1216
|
+
const authorization = headerValue(auth.headers, "Authorization");
|
|
1217
|
+
const token = authorization === void 0 ? void 0 : bearerToken(authorization) ?? authorization;
|
|
1218
|
+
if (token === void 0 || token === authorization) return auth;
|
|
1219
|
+
return { ...auth, headers: { ...auth.headers, Authorization: token } };
|
|
1060
1220
|
}
|
|
1061
1221
|
function isAbortError(error) {
|
|
1062
1222
|
return error instanceof Error && error.name === "AbortError";
|
|
@@ -1200,7 +1360,7 @@ function normalizeCodexResetCreditsPayload(payload) {
|
|
|
1200
1360
|
if (rawCredits !== void 0 && !Array.isArray(rawCredits)) {
|
|
1201
1361
|
throw new Error("Codex reset credits response returned invalid credits.");
|
|
1202
1362
|
}
|
|
1203
|
-
const options = (rawCredits ?? []).map(
|
|
1363
|
+
const options = (rawCredits ?? []).map(asObject7).filter((credit) => Boolean(credit)).filter((credit) => credit.status === "available" && credit.reset_type === "codex_rate_limits").map(normalizeResetOption).sort(
|
|
1204
1364
|
(left, right) => (left.expiresAt ?? Number.MAX_SAFE_INTEGER) - (right.expiresAt ?? Number.MAX_SAFE_INTEGER)
|
|
1205
1365
|
).slice(0, Math.min(availableCount, MAX_RESET_OPTIONS));
|
|
1206
1366
|
if (availableCount > 0 && options.length === 0) {
|
|
@@ -1215,7 +1375,7 @@ function selectCodexResetCredential(candidates, resolvedAccess, resolvedAccountI
|
|
|
1215
1375
|
const matches = /* @__PURE__ */ new Map();
|
|
1216
1376
|
for (const candidate of candidates) {
|
|
1217
1377
|
try {
|
|
1218
|
-
const credential =
|
|
1378
|
+
const credential = asObject7(candidate);
|
|
1219
1379
|
if (credential?.type !== "oauth") continue;
|
|
1220
1380
|
sawOAuth = true;
|
|
1221
1381
|
const storedAccess = asNonemptyString(credential.access);
|
|
@@ -1254,7 +1414,7 @@ function codexAccountIdFromAccessToken(access) {
|
|
|
1254
1414
|
const parts = access.split(".");
|
|
1255
1415
|
if (parts.length !== 3 || !parts[1]) return void 0;
|
|
1256
1416
|
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
1257
|
-
const claims =
|
|
1417
|
+
const claims = asObject7(asObject7(payload)?.["https://api.openai.com/auth"]);
|
|
1258
1418
|
return validHeaderValue(claims?.chatgpt_account_id);
|
|
1259
1419
|
} catch {
|
|
1260
1420
|
return void 0;
|
|
@@ -1286,7 +1446,7 @@ function normalizeResetOption(credit) {
|
|
|
1286
1446
|
function isCodexResetOutcomeCode(value) {
|
|
1287
1447
|
return value === "reset" || value === "nothing_to_reset" || value === "no_credit" || value === "already_redeemed";
|
|
1288
1448
|
}
|
|
1289
|
-
function
|
|
1449
|
+
function asObject7(value) {
|
|
1290
1450
|
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1291
1451
|
return value;
|
|
1292
1452
|
}
|
|
@@ -1334,7 +1494,9 @@ function formatUsageReport(report, displayState) {
|
|
|
1334
1494
|
else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
|
|
1335
1495
|
else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
|
|
1336
1496
|
else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
|
|
1337
|
-
else
|
|
1497
|
+
else if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
|
|
1498
|
+
formatZaiReport(lines, report);
|
|
1499
|
+
} else formatGenericReport(lines, report);
|
|
1338
1500
|
if (report.notes) {
|
|
1339
1501
|
for (const note of report.notes) lines.push(note);
|
|
1340
1502
|
}
|
|
@@ -1421,7 +1583,7 @@ function compactGitHubCopilotQuotaKind(bucket) {
|
|
|
1421
1583
|
}
|
|
1422
1584
|
function percentRemaining(bucket) {
|
|
1423
1585
|
if (!bucket.limit || bucket.remaining === void 0) return 0;
|
|
1424
|
-
return Math.round(
|
|
1586
|
+
return Math.round(clampPercent4(bucket.remaining / bucket.limit * 100));
|
|
1425
1587
|
}
|
|
1426
1588
|
function formatOpenRouterReport(lines, report) {
|
|
1427
1589
|
const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
|
|
@@ -1448,10 +1610,33 @@ function formatOpenCodeZenStatusline(report) {
|
|
|
1448
1610
|
for (const bucket of report.buckets) {
|
|
1449
1611
|
if (bucket.used === void 0) continue;
|
|
1450
1612
|
const compact = bucket.id === "rolling" ? "r" : bucket.id === "weekly" ? "w" : "m";
|
|
1451
|
-
parts.push(`${
|
|
1613
|
+
parts.push(`${clampPercent4(bucket.used).toFixed(0)}% ${compact}`);
|
|
1452
1614
|
}
|
|
1453
1615
|
return parts.length > 1 ? parts.join(" ") : void 0;
|
|
1454
1616
|
}
|
|
1617
|
+
function formatZaiReport(lines, report) {
|
|
1618
|
+
for (const bucket of report.buckets) {
|
|
1619
|
+
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
1620
|
+
let value = "unavailable";
|
|
1621
|
+
if (bucket.unit === "percent" && bucket.used !== void 0) {
|
|
1622
|
+
value = `${bucket.used}% used`;
|
|
1623
|
+
if (bucket.remaining !== void 0) value += ` \xB7 ${bucket.remaining}% left`;
|
|
1624
|
+
} else if (bucket.used !== void 0 && bucket.limit !== void 0) {
|
|
1625
|
+
value = `${bucket.used} of ${bucket.limit} used`;
|
|
1626
|
+
if (bucket.remaining !== void 0) value += ` \xB7 ${bucket.remaining} left`;
|
|
1627
|
+
} else if (bucket.used !== void 0) {
|
|
1628
|
+
value = `${bucket.used} used`;
|
|
1629
|
+
} else if (bucket.remaining !== void 0) {
|
|
1630
|
+
value = `${bucket.remaining} left`;
|
|
1631
|
+
}
|
|
1632
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}${reset}`);
|
|
1633
|
+
}
|
|
1634
|
+
for (const metric of report.metrics) {
|
|
1635
|
+
lines.push(
|
|
1636
|
+
`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1455
1640
|
function formatGenericReport(lines, report) {
|
|
1456
1641
|
for (const bucket of report.buckets) {
|
|
1457
1642
|
lines.push(
|
|
@@ -1476,7 +1661,7 @@ function formatCodexStatusline(report, model) {
|
|
|
1476
1661
|
if (bucket.remaining === void 0) continue;
|
|
1477
1662
|
const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
|
|
1478
1663
|
parts.push(
|
|
1479
|
-
`${
|
|
1664
|
+
`${clampPercent4(bucket.remaining).toFixed(0)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`
|
|
1480
1665
|
);
|
|
1481
1666
|
}
|
|
1482
1667
|
return parts.length > 1 ? parts.join(" ") : formatCodexCreditsStatus(report);
|
|
@@ -1543,7 +1728,7 @@ function compactLimitLabel(label) {
|
|
|
1543
1728
|
return (suffix || normalized).toLowerCase().replace(/\s+/g, " ");
|
|
1544
1729
|
}
|
|
1545
1730
|
function formatPercentBucket(bucket) {
|
|
1546
|
-
const remaining =
|
|
1731
|
+
const remaining = clampPercent4(bucket.remaining ?? 0);
|
|
1547
1732
|
const filled = Math.round(remaining / 100 * BAR_SEGMENTS);
|
|
1548
1733
|
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
1549
1734
|
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(BAR_SEGMENTS - filled)}] ${remaining.toFixed(0)}% left${reset}`;
|
|
@@ -1576,7 +1761,7 @@ function formatReset(epochSeconds) {
|
|
|
1576
1761
|
function capitalize(value) {
|
|
1577
1762
|
return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
|
|
1578
1763
|
}
|
|
1579
|
-
function
|
|
1764
|
+
function clampPercent4(value) {
|
|
1580
1765
|
return Math.min(100, Math.max(0, value));
|
|
1581
1766
|
}
|
|
1582
1767
|
|
|
@@ -1975,6 +2160,11 @@ function usageExtension(pi, dependencies = {}) {
|
|
|
1975
2160
|
statusRefreshTimer.unref?.();
|
|
1976
2161
|
};
|
|
1977
2162
|
const publishStatus = (ctx, outcome, model, shouldSchedule) => {
|
|
2163
|
+
if (adapterForProvider(model.provider)?.publishesStatusline === false) {
|
|
2164
|
+
clearStatusTimer();
|
|
2165
|
+
safeSetStatus(ctx, void 0);
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
1978
2168
|
if (outcome.state.status === "unsupported") {
|
|
1979
2169
|
clearStatusTimer();
|
|
1980
2170
|
safeSetStatus(ctx, void 0);
|
|
@@ -2157,6 +2347,10 @@ function usageExtension(pi, dependencies = {}) {
|
|
|
2157
2347
|
clearStatus(ctx);
|
|
2158
2348
|
return;
|
|
2159
2349
|
}
|
|
2350
|
+
if (adapter.publishesStatusline === false) {
|
|
2351
|
+
clearStatus(ctx);
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2160
2354
|
statusGeneration += 1;
|
|
2161
2355
|
const generation = statusGeneration;
|
|
2162
2356
|
statusController?.abort();
|
|
@@ -2747,6 +2941,7 @@ export {
|
|
|
2747
2941
|
normalizeOpenCodeZenPayload,
|
|
2748
2942
|
normalizeOpenRouterKeyPayload,
|
|
2749
2943
|
normalizeUsageSettings,
|
|
2944
|
+
normalizeZaiQuotaPayload,
|
|
2750
2945
|
providerIsConfigured,
|
|
2751
2946
|
queryProviderUsage,
|
|
2752
2947
|
redactUsageError,
|