@narumitw/pi-usage 0.60.4 → 0.60.7
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/dist/index.ts +131 -63
- package/dist/index.ts.map +4 -4
- package/docs/providers.md +8 -1
- package/package.json +2 -2
- package/src/core.ts +4 -4
- package/src/providers/zai-errors.ts +94 -0
- package/src/providers/zai.ts +7 -13
- package/src/query.ts +10 -11
- package/src/types.ts +6 -1
- package/src/usage-settings-ui.ts +21 -39
- package/src/usage.ts +6 -12
package/docs/providers.md
CHANGED
|
@@ -259,6 +259,13 @@ The extension classifies both forms by the provider's window unit and does not l
|
|
|
259
259
|
The quota monitor expects a raw API key without a `Bearer` prefix.
|
|
260
260
|
The extension removes that prefix from resolved authorization before sending it to the monitor endpoint.
|
|
261
261
|
Fingerprinting and redaction keep using the original resolved credential.
|
|
262
|
-
|
|
262
|
+
Quota errors use fixed English hints for the [documented Z.AI business error codes](https://docs.z.ai/api-reference/api-code), accepting nested `error.code` and the monitor endpoint's top-level `code` as strings or numbers.
|
|
263
|
+
An explicit nested code takes precedence; when it is absent, the top-level code is used.
|
|
264
|
+
For example, `1113` reports insufficient balance or no resource package, and `1309` reports an expired GLM Coding Plan.
|
|
265
|
+
When a failed HTTP response has no business error code, the hint uses its HTTP status; unknown business codes use a generic API failure message.
|
|
266
|
+
The monitor's top-level `code: 500` is not a documented business code and is not treated as HTTP 500 or proof that the credential has no Coding Plan.
|
|
267
|
+
Error classification never matches or echoes provider `msg` or `error.message` text, which may contain credentials.
|
|
268
|
+
Quota errors and missing or malformed quota data remain query failures with a statusline error hint, a 30-second request backoff, and scheduled retries; they no longer clear the statusline as unsupported.
|
|
269
|
+
An accepted Z.AI query failure discards the matching cached report, so the next turn after backoff expires retries instead of restoring stale usage.
|
|
263
270
|
The plan endpoint only contributes the plan name and renewal date; when it is unavailable or fails, the quota windows remain reported and the plan note falls back to the quota response's plan level.
|
|
264
271
|
Only the official `api.z.ai` and `open.bigmodel.cn` origins are queried; other origins fail before sending the credential.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-usage",
|
|
3
|
-
"version": "0.60.
|
|
3
|
+
"version": "0.60.7",
|
|
4
4
|
"description": "Pi extension that shows current-account usage and DeepSeek API balance for supported providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,6 +71,6 @@
|
|
|
71
71
|
"directory": "packages/pi-usage"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@narumitw/pi-tui-kit": "^0.
|
|
74
|
+
"@narumitw/pi-tui-kit": "^0.61.0"
|
|
75
75
|
}
|
|
76
76
|
}
|
package/src/core.ts
CHANGED
|
@@ -36,6 +36,10 @@ export class UsageCache {
|
|
|
36
36
|
this.entries.set(key, { createdAt: now, report });
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
delete(providerId: string, fingerprint: string): void {
|
|
40
|
+
this.entries.delete(cacheKey(providerId, fingerprint));
|
|
41
|
+
}
|
|
42
|
+
|
|
39
43
|
clearProvider(providerId: string): void {
|
|
40
44
|
for (const key of this.entries.keys()) {
|
|
41
45
|
if (key.startsWith(`${providerId}:`)) this.entries.delete(key);
|
|
@@ -198,10 +202,6 @@ export function errorMessage(error: unknown): string {
|
|
|
198
202
|
return sanitizeDisplayText(error instanceof Error ? error.message : String(error), 600);
|
|
199
203
|
}
|
|
200
204
|
|
|
201
|
-
// A credential the provider cannot meter is not a query failure: it is reported as unsupported, so
|
|
202
|
-
// the statusline stays empty instead of holding an error chip that no retry will clear.
|
|
203
|
-
export class UsageUnsupportedError extends Error {}
|
|
204
|
-
|
|
205
205
|
export function abortError(): Error {
|
|
206
206
|
return Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
|
|
207
207
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Fixed messages from https://docs.z.ai/api-reference/api-code. Never interpolate provider text:
|
|
2
|
+
// error messages and their template parameters can contain credentials or terminal controls.
|
|
3
|
+
const ERROR_MESSAGES: Readonly<Record<string, string>> = {
|
|
4
|
+
"1000": "Authentication failed. Check your API key.",
|
|
5
|
+
"1001": "Authentication header missing. Check your API key.",
|
|
6
|
+
"1003": "Authentication token expired. Obtain a new token.",
|
|
7
|
+
"1005": "Two-factor authentication required.",
|
|
8
|
+
"1113": "Insufficient balance or no resource package. Recharge your account.",
|
|
9
|
+
"1200": "API call error. Try again later.",
|
|
10
|
+
"1210": "Invalid API parameter. Check the API documentation.",
|
|
11
|
+
"1211": "Unknown model. Check the model ID.",
|
|
12
|
+
"1212": "This model does not support the requested method.",
|
|
13
|
+
"1213": "A required parameter is missing.",
|
|
14
|
+
"1214": "Invalid parameter. Check the API documentation.",
|
|
15
|
+
"1215": "Conflicting parameters. Check the API documentation.",
|
|
16
|
+
"1220": "Access denied. Check your permissions.",
|
|
17
|
+
"1221": "This API has been taken offline.",
|
|
18
|
+
"1222": "This API does not exist.",
|
|
19
|
+
"1230": "API processing error. Try again later.",
|
|
20
|
+
"1234": "Network error. Try again later.",
|
|
21
|
+
"1261": "Prompt too long. Reduce the input length.",
|
|
22
|
+
"1301": "Content rejected by the safety policy.",
|
|
23
|
+
"1302": "Request rate limit reached. Try again later.",
|
|
24
|
+
"1305": "Service overloaded. Try again later.",
|
|
25
|
+
"1308": "Usage limit reached. Wait for the quota reset.",
|
|
26
|
+
"1309": "GLM Coding Plan expired. Renew your subscription.",
|
|
27
|
+
"1310": "Weekly or monthly limit exhausted. Wait for the quota reset.",
|
|
28
|
+
"1311": "Your subscription does not include this model.",
|
|
29
|
+
"1313": "Request frequency restricted by the Fair Usage Policy. Contact support.",
|
|
30
|
+
"1314": "Enterprise package expired. Contact your administrator.",
|
|
31
|
+
"1315": "This API key requires an enterprise coding package scenario. Replace the key.",
|
|
32
|
+
"1316": "5-hour limit reached; insufficient balance for extra usage. Wait for reset.",
|
|
33
|
+
"1317": "7-day limit reached; insufficient balance for extra usage. Wait for reset.",
|
|
34
|
+
"1318": "5-hour limit reached; extra usage blocked by monthly spend limit.",
|
|
35
|
+
"1319": "7-day limit reached; extra usage blocked by monthly spend limit.",
|
|
36
|
+
"1320": "5-hour limit reached; extra usage blocked by monthly spend limit.",
|
|
37
|
+
"1321": "7-day limit reached; extra usage blocked by monthly spend limit.",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const HTTP_MESSAGES: Readonly<Record<number, string>> = {
|
|
41
|
+
400: "Invalid request. Check the API documentation.",
|
|
42
|
+
401: "Authentication failed. Check your API key.",
|
|
43
|
+
403: "Access denied. Check your permissions.",
|
|
44
|
+
429: "Request or usage limit reached. Try again later.",
|
|
45
|
+
500: "Internal error. Try again later.",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function zaiPayloadError(payload: unknown): string | undefined {
|
|
49
|
+
const object = asObject(payload);
|
|
50
|
+
if (!object) return undefined;
|
|
51
|
+
const nested = asObject(object.error);
|
|
52
|
+
// Missing nested metadata must not hide a top-level code; explicit nested values retain priority.
|
|
53
|
+
const rawCode = nested?.code === undefined ? object.code : nested.code;
|
|
54
|
+
const code = errorCode(rawCode);
|
|
55
|
+
if (
|
|
56
|
+
object.error === undefined &&
|
|
57
|
+
object.success !== false &&
|
|
58
|
+
(rawCode === undefined || code === "0" || code === "200")
|
|
59
|
+
) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return code && code !== "0" && code !== "200"
|
|
63
|
+
? `Z.AI ${code}: ${ERROR_MESSAGES[code] ?? "API request failed."}`
|
|
64
|
+
: "Z.AI: API request failed.";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function zaiResponseError(status: number, text: string): string | undefined {
|
|
68
|
+
let payload: unknown;
|
|
69
|
+
try {
|
|
70
|
+
payload = JSON.parse(text) as unknown;
|
|
71
|
+
} catch {
|
|
72
|
+
// Do not expose JSON parser excerpts, response bodies, or statusText.
|
|
73
|
+
if (status >= 200 && status < 300) return "Z.AI: Invalid JSON response.";
|
|
74
|
+
}
|
|
75
|
+
const error = zaiPayloadError(payload);
|
|
76
|
+
if (error) return error;
|
|
77
|
+
if (status < 200 || status >= 300) {
|
|
78
|
+
return `Z.AI HTTP ${status}: ${HTTP_MESSAGES[status] ?? "API request failed."}`;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function errorCode(value: unknown): string | undefined {
|
|
84
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 9999) {
|
|
85
|
+
return String(value);
|
|
86
|
+
}
|
|
87
|
+
return typeof value === "string" && /^(?:0|[1-9]\d{0,3})$/u.test(value) ? value : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
91
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
92
|
+
? (value as Record<string, unknown>)
|
|
93
|
+
: undefined;
|
|
94
|
+
}
|
package/src/providers/zai.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sanitizeDisplayText
|
|
1
|
+
import { sanitizeDisplayText } from "../core.js";
|
|
2
2
|
import type {
|
|
3
3
|
UsageBucket,
|
|
4
4
|
UsageMetric,
|
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
ZaiSubscriptionPayload,
|
|
9
9
|
} from "../types.js";
|
|
10
10
|
|
|
11
|
+
import { zaiPayloadError } from "./zai-errors.js";
|
|
12
|
+
|
|
11
13
|
const FIVE_HOUR_WINDOW_MINUTES = 300;
|
|
12
14
|
const WEEKLY_WINDOW_MINUTES = 10_080;
|
|
13
15
|
|
|
@@ -18,15 +20,10 @@ export function normalizeZaiQuotaPayload(
|
|
|
18
20
|
capturedAt: number,
|
|
19
21
|
plan?: ZaiPlanInfo,
|
|
20
22
|
): UsageReport {
|
|
23
|
+
const error = zaiPayloadError(payload);
|
|
24
|
+
if (error) throw new Error(error);
|
|
21
25
|
const data = asObject(payload.data);
|
|
22
|
-
|
|
23
|
-
// {"code":500,"msg":"当前用户不存在coding plan","success":false}. API usage is not metered.
|
|
24
|
-
if (!data) {
|
|
25
|
-
const reason = asString(payload.msg);
|
|
26
|
-
throw new UsageUnsupportedError(
|
|
27
|
-
`No GLM Coding Plan on this Z.AI credential; API usage is not metered.${reason ? ` Z.AI reported: ${reason}` : ""}`,
|
|
28
|
-
);
|
|
29
|
-
}
|
|
26
|
+
if (!data) throw new Error("Z.AI quota response data was not an object.");
|
|
30
27
|
const limits = Array.isArray(data.limits) ? (data.limits as unknown[]) : [];
|
|
31
28
|
|
|
32
29
|
const buckets: UsageBucket[] = [];
|
|
@@ -88,10 +85,7 @@ export function normalizeZaiQuotaPayload(
|
|
|
88
85
|
export function normalizeZaiSubscriptionPayload(
|
|
89
86
|
payload: ZaiSubscriptionPayload,
|
|
90
87
|
): ZaiPlanInfo | undefined {
|
|
91
|
-
if (payload
|
|
92
|
-
if (typeof payload.code === "number" && payload.code !== 0 && payload.code !== 200) {
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
88
|
+
if (zaiPayloadError(payload)) return undefined;
|
|
95
89
|
if (!Array.isArray(payload.data)) return undefined;
|
|
96
90
|
|
|
97
91
|
const candidates: Array<{
|
package/src/query.ts
CHANGED
|
@@ -2,12 +2,7 @@
|
|
|
2
2
|
// separating that security boundary would duplicate request and redaction policy across providers.
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { type ExtensionContext, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
6
|
-
errorMessage,
|
|
7
|
-
fingerprintResolvedAuth,
|
|
8
|
-
redactUsageError,
|
|
9
|
-
UsageUnsupportedError,
|
|
10
|
-
} from "./core.js";
|
|
5
|
+
import { errorMessage, fingerprintResolvedAuth, redactUsageError } from "./core.js";
|
|
11
6
|
import {
|
|
12
7
|
fallbackOAuthCredentialCandidates,
|
|
13
8
|
type OAuthCredentialCandidateReader,
|
|
@@ -29,6 +24,7 @@ import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
|
29
24
|
import { normalizeVercelAIGatewayCreditsPayload } from "./providers/vercel-ai-gateway.js";
|
|
30
25
|
import { normalizeXaiBillingPayload } from "./providers/xai.js";
|
|
31
26
|
import { normalizeZaiQuotaPayload, normalizeZaiSubscriptionPayload } from "./providers/zai.js";
|
|
27
|
+
import { zaiResponseError } from "./providers/zai-errors.js";
|
|
32
28
|
import type {
|
|
33
29
|
BasetenBillingUsagePayload,
|
|
34
30
|
CodexBackendPayload,
|
|
@@ -266,6 +262,7 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
266
262
|
{
|
|
267
263
|
id: "zai",
|
|
268
264
|
displayName: "Z.AI",
|
|
265
|
+
invalidateCacheOnFailure: true,
|
|
269
266
|
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
270
267
|
async query(auth, signal, timeoutMs, guard) {
|
|
271
268
|
return queryZaiUsage("zai", "Z.AI", auth, signal, timeoutMs, guard);
|
|
@@ -274,6 +271,7 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
274
271
|
{
|
|
275
272
|
id: "zai-coding-cn",
|
|
276
273
|
displayName: "Z.AI Coding CN",
|
|
274
|
+
invalidateCacheOnFailure: true,
|
|
277
275
|
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
278
276
|
async query(auth, signal, timeoutMs, guard) {
|
|
279
277
|
return queryZaiUsage("zai-coding-cn", "Z.AI Coding CN", auth, signal, timeoutMs, guard);
|
|
@@ -552,11 +550,7 @@ export async function queryProviderUsage(
|
|
|
552
550
|
);
|
|
553
551
|
} catch (error) {
|
|
554
552
|
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
555
|
-
|
|
556
|
-
// Redaction rebuilds the error, so the unsupported signal has to be carried across.
|
|
557
|
-
throw error instanceof UsageUnsupportedError
|
|
558
|
-
? new UsageUnsupportedError(message)
|
|
559
|
-
: new Error(message);
|
|
553
|
+
throw new Error(redactUsageError(errorMessage(error), auth.secrets));
|
|
560
554
|
}
|
|
561
555
|
}
|
|
562
556
|
|
|
@@ -636,6 +630,7 @@ export async function fetchProviderJson(
|
|
|
636
630
|
body?: Record<string, unknown>;
|
|
637
631
|
redirect?: RequestRedirect;
|
|
638
632
|
userAgent?: boolean;
|
|
633
|
+
responseError?: (status: number, text: string) => string | undefined;
|
|
639
634
|
} = {},
|
|
640
635
|
): Promise<Record<string, unknown>> {
|
|
641
636
|
const controller = new AbortController();
|
|
@@ -674,6 +669,8 @@ export async function fetchProviderJson(
|
|
|
674
669
|
);
|
|
675
670
|
if (controller.signal.aborted)
|
|
676
671
|
throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
|
|
672
|
+
const responseError = request.responseError?.(response.status, text);
|
|
673
|
+
if (responseError) throw new Error(responseError);
|
|
677
674
|
if (!response.ok) {
|
|
678
675
|
throw new Error(
|
|
679
676
|
`${description} returned ${response.status} ${response.statusText}: ${redactUsageError(text, auth.secrets)}`,
|
|
@@ -1110,6 +1107,7 @@ async function queryZaiUsage(
|
|
|
1110
1107
|
signal,
|
|
1111
1108
|
remainingTimeout(timeoutMs, startedAt, `fetching ${providerName} quota`),
|
|
1112
1109
|
`${providerName} quota endpoint`,
|
|
1110
|
+
{ responseError: zaiResponseError },
|
|
1113
1111
|
)) as ZaiQuotaPayload;
|
|
1114
1112
|
await guard();
|
|
1115
1113
|
const planTimeoutMs = timeoutMs - (Date.now() - startedAt);
|
|
@@ -1134,6 +1132,7 @@ async function fetchZaiPlan(
|
|
|
1134
1132
|
signal,
|
|
1135
1133
|
timeoutMs,
|
|
1136
1134
|
`${providerName} plan endpoint`,
|
|
1135
|
+
{ responseError: zaiResponseError },
|
|
1137
1136
|
)) as ZaiSubscriptionPayload;
|
|
1138
1137
|
return normalizeZaiSubscriptionPayload(payload);
|
|
1139
1138
|
} catch (error) {
|
package/src/types.ts
CHANGED
|
@@ -91,6 +91,8 @@ export interface UsageProviderAdapter {
|
|
|
91
91
|
displayName: string;
|
|
92
92
|
semantics: UsageSemantics;
|
|
93
93
|
publishesStatusline?: boolean;
|
|
94
|
+
/** Invalidate the matching ready report when the latest query fails; default preserves it. */
|
|
95
|
+
invalidateCacheOnFailure?: boolean;
|
|
94
96
|
targets?: UsageTargetResolver;
|
|
95
97
|
query(
|
|
96
98
|
auth: ResolvedUsageAuth,
|
|
@@ -191,11 +193,14 @@ export type OpenCodeZenPayload = {
|
|
|
191
193
|
};
|
|
192
194
|
|
|
193
195
|
export type ZaiQuotaPayload = {
|
|
194
|
-
|
|
196
|
+
error?: unknown;
|
|
197
|
+
code?: unknown;
|
|
198
|
+
success?: unknown;
|
|
195
199
|
data?: unknown;
|
|
196
200
|
};
|
|
197
201
|
|
|
198
202
|
export type ZaiSubscriptionPayload = {
|
|
203
|
+
error?: unknown;
|
|
199
204
|
code?: unknown;
|
|
200
205
|
success?: unknown;
|
|
201
206
|
data?: unknown;
|
package/src/usage-settings-ui.ts
CHANGED
|
@@ -3,14 +3,7 @@ import {
|
|
|
3
3
|
type ExtensionCommandContext,
|
|
4
4
|
getSettingsListTheme,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
Container,
|
|
8
|
-
Key,
|
|
9
|
-
matchesKey,
|
|
10
|
-
type SettingItem,
|
|
11
|
-
SettingsList,
|
|
12
|
-
Text,
|
|
13
|
-
} from "@earendil-works/pi-tui";
|
|
6
|
+
import { Key, matchesKey, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
|
|
14
7
|
import { errorMessage } from "./core.js";
|
|
15
8
|
import type { UsageSettingsRuntime } from "./settings.js";
|
|
16
9
|
|
|
@@ -19,26 +12,6 @@ const ON = "On";
|
|
|
19
12
|
|
|
20
13
|
type UsageSettingId = "codexFastMode" | "codexStatusResetCountdown";
|
|
21
14
|
|
|
22
|
-
function fitSettingsRows(lines: string[], maxRows: number): string[] {
|
|
23
|
-
if (lines.length <= maxRows) return lines;
|
|
24
|
-
const content = lines.filter((line) => stripVTControlCharacters(line).trim().length > 0);
|
|
25
|
-
if (content.length <= maxRows) return content;
|
|
26
|
-
const selectedIndex = content.findIndex((line) =>
|
|
27
|
-
/^[→›]\s/u.test(stripVTControlCharacters(line)),
|
|
28
|
-
);
|
|
29
|
-
if (selectedIndex < 0) return content.slice(0, maxRows);
|
|
30
|
-
return content
|
|
31
|
-
.map((line, index) => ({ index, line }))
|
|
32
|
-
.sort(
|
|
33
|
-
(left, right) =>
|
|
34
|
-
Math.abs(left.index - selectedIndex) - Math.abs(right.index - selectedIndex) ||
|
|
35
|
-
left.index - right.index,
|
|
36
|
-
)
|
|
37
|
-
.slice(0, maxRows)
|
|
38
|
-
.sort((left, right) => left.index - right.index)
|
|
39
|
-
.map(({ line }) => line);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
15
|
export async function showUsageSettings(
|
|
43
16
|
ctx: ExtensionCommandContext,
|
|
44
17
|
settingsRuntime: UsageSettingsRuntime,
|
|
@@ -50,7 +23,7 @@ export async function showUsageSettings(
|
|
|
50
23
|
if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${settingsRuntime.get().path}`, "info");
|
|
51
24
|
return false;
|
|
52
25
|
}
|
|
53
|
-
const { HorizontalRule } = await import("@narumitw/pi-tui-kit");
|
|
26
|
+
const { HorizontalRule, renderBoundedFrame } = await import("@narumitw/pi-tui-kit");
|
|
54
27
|
if (parentSignal.aborted || !isCurrent()) return false;
|
|
55
28
|
return (
|
|
56
29
|
(await ctx.ui.custom<boolean>((tui, theme, _keybindings, done) => {
|
|
@@ -76,9 +49,7 @@ export async function showUsageSettings(
|
|
|
76
49
|
values: [OFF, ON],
|
|
77
50
|
},
|
|
78
51
|
];
|
|
79
|
-
const container = new Container();
|
|
80
52
|
const rule = new HorizontalRule({ ruleStyle: (text) => theme.fg("border", text) });
|
|
81
|
-
container.addChild(new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 1));
|
|
82
53
|
|
|
83
54
|
let settingsList: SettingsList;
|
|
84
55
|
const cancel = () => {
|
|
@@ -126,23 +97,34 @@ export async function showUsageSettings(
|
|
|
126
97
|
},
|
|
127
98
|
cancel,
|
|
128
99
|
);
|
|
129
|
-
container.addChild(settingsList);
|
|
130
100
|
|
|
131
101
|
parentSignal.addEventListener("abort", cancel, { once: true });
|
|
132
102
|
return {
|
|
133
103
|
render(width: number) {
|
|
134
|
-
|
|
104
|
+
// Compatibility: Kit puts the title directly below the top rule and keeps compact
|
|
105
|
+
// rules when at least five rows fit; both replace the legacy wrapper layout.
|
|
106
|
+
const title = new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 0).render(
|
|
107
|
+
width,
|
|
108
|
+
);
|
|
109
|
+
const content = settingsList.render(width);
|
|
110
|
+
const focusedRow = content.findIndex((line) =>
|
|
111
|
+
/^[→›]\s/u.test(stripVTControlCharacters(line)),
|
|
112
|
+
);
|
|
135
113
|
const terminalRows = Number.isFinite(tui.terminal?.rows)
|
|
136
114
|
? Math.floor(tui.terminal.rows)
|
|
137
115
|
: 24;
|
|
138
|
-
const availableRows = Math.max(1, terminalRows - 3);
|
|
139
|
-
if (content.length + 2 > availableRows) {
|
|
140
|
-
return fitSettingsRows(content, availableRows);
|
|
141
|
-
}
|
|
142
116
|
const [ruleLine = ""] = rule.render(width);
|
|
143
|
-
return
|
|
117
|
+
return renderBoundedFrame({
|
|
118
|
+
width,
|
|
119
|
+
maxRows: Math.max(1, terminalRows - 3),
|
|
120
|
+
rule: ruleLine,
|
|
121
|
+
title,
|
|
122
|
+
content,
|
|
123
|
+
priorityRows: focusedRow < 0 ? [] : [focusedRow],
|
|
124
|
+
focusedRow,
|
|
125
|
+
});
|
|
144
126
|
},
|
|
145
|
-
invalidate: () =>
|
|
127
|
+
invalidate: () => settingsList.invalidate(),
|
|
146
128
|
handleInput(data: string) {
|
|
147
129
|
if (closing) return;
|
|
148
130
|
if (matchesKey(data, Key.ctrl("c"))) cancel();
|
package/src/usage.ts
CHANGED
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
errorMessage,
|
|
29
29
|
runWithConcurrency,
|
|
30
30
|
UsageCache,
|
|
31
|
-
UsageUnsupportedError,
|
|
32
31
|
} from "./core.js";
|
|
33
32
|
import { formatProviderStates, formatUsageStatusline } from "./format.js";
|
|
34
33
|
import { createOAuthCredentialCandidateReader } from "./oauth-credential-source.js";
|
|
@@ -111,10 +110,7 @@ export default function usageExtension(
|
|
|
111
110
|
const createRedemptionId = dependencies.createRedemptionId ?? randomUUID;
|
|
112
111
|
const settingsRuntime = dependencies.settingsRuntime ?? createUsageSettingsRuntime();
|
|
113
112
|
const cache = new UsageCache(CACHE_TTL_MS);
|
|
114
|
-
const failureBackoff = new Map<
|
|
115
|
-
string,
|
|
116
|
-
{ until: number; message: string; status: "query-failed" | "unsupported" }
|
|
117
|
-
>();
|
|
113
|
+
const failureBackoff = new Map<string, { until: number; message: string }>();
|
|
118
114
|
const latestQueries = new Map<string, number>();
|
|
119
115
|
const activeControllers = new Set<AbortController>();
|
|
120
116
|
let querySequence = 0;
|
|
@@ -363,7 +359,7 @@ export default function usageExtension(
|
|
|
363
359
|
providerId: adapter.id,
|
|
364
360
|
providerName,
|
|
365
361
|
displayState,
|
|
366
|
-
status:
|
|
362
|
+
status: "query-failed",
|
|
367
363
|
message: previousDiscoveryFailure.message,
|
|
368
364
|
},
|
|
369
365
|
fingerprint: auth.fingerprint,
|
|
@@ -424,7 +420,7 @@ export default function usageExtension(
|
|
|
424
420
|
providerId: adapter.id,
|
|
425
421
|
providerName,
|
|
426
422
|
displayState,
|
|
427
|
-
status:
|
|
423
|
+
status: "query-failed",
|
|
428
424
|
message: previousFailure.message,
|
|
429
425
|
},
|
|
430
426
|
fingerprint: auth.fingerprint,
|
|
@@ -483,18 +479,16 @@ export default function usageExtension(
|
|
|
483
479
|
);
|
|
484
480
|
}
|
|
485
481
|
const message = errorMessage(error);
|
|
486
|
-
const status = error instanceof UsageUnsupportedError ? "unsupported" : "query-failed";
|
|
487
482
|
const now = Date.now();
|
|
488
483
|
for (const [key, failure] of failureBackoff) {
|
|
489
484
|
if (failure.until <= now) failureBackoff.delete(key);
|
|
490
485
|
}
|
|
491
|
-
// Unsupported is throttled like a failure, so a credential the provider cannot meter does
|
|
492
|
-
// not re-query on every turn; the entry carries its status so the replay stays unsupported.
|
|
493
486
|
if (queryId === undefined || latestQueries.get(failureKey) === queryId) {
|
|
487
|
+
if (adapter.invalidateCacheOnFailure) cache.delete(adapter.id, queryFingerprint);
|
|
494
488
|
setBoundedMap(
|
|
495
489
|
failureBackoff,
|
|
496
490
|
failureKey,
|
|
497
|
-
{ until: now + FAILURE_BACKOFF_MS, message
|
|
491
|
+
{ until: now + FAILURE_BACKOFF_MS, message },
|
|
498
492
|
MAX_ACCOUNT_STATES,
|
|
499
493
|
);
|
|
500
494
|
}
|
|
@@ -503,7 +497,7 @@ export default function usageExtension(
|
|
|
503
497
|
providerId: adapter.id,
|
|
504
498
|
providerName,
|
|
505
499
|
displayState,
|
|
506
|
-
status,
|
|
500
|
+
status: "query-failed",
|
|
507
501
|
message,
|
|
508
502
|
},
|
|
509
503
|
fingerprint: auth.fingerprint,
|