@narumitw/pi-codex-usage 0.1.15 → 0.1.16
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 +4 -1
- package/package.json +1 -1
- package/src/codex-usage.ts +124 -17
package/README.md
CHANGED
|
@@ -67,9 +67,12 @@ When the selected Pi model provider is `openai-codex`, `pi-codex-usage` refreshe
|
|
|
67
67
|
|
|
68
68
|
```text
|
|
69
69
|
codex 59% 5h 61% wk
|
|
70
|
+
codex spark 100% 5h 100% wk
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
The statusline value uses the cached usage snapshot and refreshes every five minutes while the current model remains `openai-codex`.
|
|
73
|
+
The statusline value uses the cached usage snapshot and refreshes every five minutes while the current model remains `openai-codex`.
|
|
74
|
+
When the selected model has its own returned usage bucket, such as `gpt-5.3-codex-spark`, the statusline switches to that bucket instead of the default `codex` bucket.
|
|
75
|
+
Switching away from an OpenAI Codex model clears the item.
|
|
73
76
|
|
|
74
77
|
Use `/codex-status --no-statusline` for a one-off notification without updating the statusline, or `/codex-status --clear-statusline` to clear the item manually.
|
|
75
78
|
|
package/package.json
CHANGED
package/src/codex-usage.ts
CHANGED
|
@@ -20,6 +20,7 @@ const RESET_FOREGROUND = "\x1b[39m";
|
|
|
20
20
|
|
|
21
21
|
type UsageSource = "pi-auth" | "codex-app-server";
|
|
22
22
|
type PiModel = NonNullable<ExtensionContext["model"]>;
|
|
23
|
+
export type CodexUsageModel = Pick<PiModel, "id" | "name" | "provider">;
|
|
23
24
|
|
|
24
25
|
type QueryUsageOptions = {
|
|
25
26
|
clearStatusline: boolean;
|
|
@@ -176,17 +177,21 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
176
177
|
const setUsageStatusline = (
|
|
177
178
|
ctx: ExtensionContext,
|
|
178
179
|
report: CodexUsageReport,
|
|
179
|
-
options: { autoRefresh: boolean },
|
|
180
|
+
options: { autoRefresh: boolean; model: CodexUsageModel | undefined },
|
|
180
181
|
) => {
|
|
181
182
|
if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
|
|
182
183
|
statuslineClearTimer = undefined;
|
|
183
|
-
ctx.ui.setStatus(STATUS_KEY, formatCodexUsageStatusline(report));
|
|
184
|
+
ctx.ui.setStatus(STATUS_KEY, formatCodexUsageStatusline(report, options.model));
|
|
184
185
|
if (options.autoRefresh) scheduleStatuslineRefresh(ctx);
|
|
185
186
|
else scheduleTemporaryStatuslineClear(ctx);
|
|
186
187
|
};
|
|
187
188
|
|
|
188
|
-
const refreshCurrentCodexUsageStatusline = async (
|
|
189
|
-
|
|
189
|
+
const refreshCurrentCodexUsageStatusline = async (
|
|
190
|
+
ctx: ExtensionContext,
|
|
191
|
+
force: boolean,
|
|
192
|
+
model = ctx.model,
|
|
193
|
+
) => {
|
|
194
|
+
if (!isOpenAICodexModel(model)) {
|
|
190
195
|
clearUsageStatusline(ctx);
|
|
191
196
|
return;
|
|
192
197
|
}
|
|
@@ -195,7 +200,7 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
195
200
|
statuslineRequestId = requestId;
|
|
196
201
|
const cached = cache && Date.now() - cache.createdAt < CACHE_TTL_MS ? cache : undefined;
|
|
197
202
|
if (cached && !force) {
|
|
198
|
-
setUsageStatusline(ctx, cached.report, { autoRefresh: true });
|
|
203
|
+
setUsageStatusline(ctx, cached.report, { autoRefresh: true, model });
|
|
199
204
|
return;
|
|
200
205
|
}
|
|
201
206
|
|
|
@@ -214,7 +219,7 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
214
219
|
}
|
|
215
220
|
|
|
216
221
|
cache = { createdAt: Date.now(), report: result.report };
|
|
217
|
-
setUsageStatusline(ctx, result.report, { autoRefresh: true });
|
|
222
|
+
setUsageStatusline(ctx, result.report, { autoRefresh: true, model });
|
|
218
223
|
};
|
|
219
224
|
|
|
220
225
|
pi.registerCommand(COMMAND_NAME, {
|
|
@@ -235,7 +240,10 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
235
240
|
const cached = cache && Date.now() - cache.createdAt < CACHE_TTL_MS ? cache : undefined;
|
|
236
241
|
if (cached && !options.value.refresh) {
|
|
237
242
|
if (options.value.statusline) {
|
|
238
|
-
setUsageStatusline(ctx, cached.report, {
|
|
243
|
+
setUsageStatusline(ctx, cached.report, {
|
|
244
|
+
autoRefresh: isOpenAICodexModel(ctx.model),
|
|
245
|
+
model: ctx.model,
|
|
246
|
+
});
|
|
239
247
|
}
|
|
240
248
|
showReport(ctx, cached.report, true);
|
|
241
249
|
return;
|
|
@@ -252,7 +260,10 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
252
260
|
|
|
253
261
|
cache = { createdAt: Date.now(), report: result.report };
|
|
254
262
|
if (options.value.statusline) {
|
|
255
|
-
setUsageStatusline(ctx, result.report, {
|
|
263
|
+
setUsageStatusline(ctx, result.report, {
|
|
264
|
+
autoRefresh: isOpenAICodexModel(ctx.model),
|
|
265
|
+
model: ctx.model,
|
|
266
|
+
});
|
|
256
267
|
keepStatusline = true;
|
|
257
268
|
}
|
|
258
269
|
showReport(ctx, result.report, false);
|
|
@@ -273,8 +284,11 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
273
284
|
});
|
|
274
285
|
|
|
275
286
|
pi.on("model_select", (event, ctx) => {
|
|
276
|
-
if (isOpenAICodexModel(event.model))
|
|
277
|
-
|
|
287
|
+
if (isOpenAICodexModel(event.model)) {
|
|
288
|
+
void refreshCurrentCodexUsageStatusline(ctx, false, event.model);
|
|
289
|
+
} else {
|
|
290
|
+
clearUsageStatusline(ctx);
|
|
291
|
+
}
|
|
278
292
|
});
|
|
279
293
|
|
|
280
294
|
pi.on("session_shutdown", (_event, ctx) => clearUsageStatusline(ctx));
|
|
@@ -324,7 +338,7 @@ function parseArgs(
|
|
|
324
338
|
return { ok: true, value: { clearStatusline, refresh, statusline, timeoutMs } };
|
|
325
339
|
}
|
|
326
340
|
|
|
327
|
-
function isOpenAICodexModel(model:
|
|
341
|
+
function isOpenAICodexModel(model: Pick<PiModel, "provider"> | undefined): boolean {
|
|
328
342
|
return model?.provider === CODEX_PROVIDER_ID;
|
|
329
343
|
}
|
|
330
344
|
|
|
@@ -806,19 +820,109 @@ export function formatCodexUsageReport(report: CodexUsageReport, _cacheAgeMs?: n
|
|
|
806
820
|
return lines.join("\n");
|
|
807
821
|
}
|
|
808
822
|
|
|
809
|
-
export function formatCodexUsageStatusline(
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
823
|
+
export function formatCodexUsageStatusline(
|
|
824
|
+
report: CodexUsageReport,
|
|
825
|
+
model?: CodexUsageModel,
|
|
826
|
+
): string {
|
|
827
|
+
const snapshot = selectSnapshotForModel(report, model);
|
|
813
828
|
if (!snapshot) return "codex usage unavailable";
|
|
814
829
|
|
|
815
|
-
const parts = [
|
|
830
|
+
const parts = [formatStatuslinePrefix(snapshot)];
|
|
816
831
|
if (snapshot.primary) parts.push(`${formatRemainingPercent(snapshot.primary)} 5h`);
|
|
817
832
|
if (snapshot.secondary) parts.push(`${formatRemainingPercent(snapshot.secondary)} wk`);
|
|
818
833
|
if (parts.length === 1 && snapshot.credits) parts.push(formatCredits(snapshot.credits));
|
|
819
834
|
return parts.join(" ");
|
|
820
835
|
}
|
|
821
836
|
|
|
837
|
+
function selectSnapshotForModel(
|
|
838
|
+
report: CodexUsageReport,
|
|
839
|
+
model: CodexUsageModel | undefined,
|
|
840
|
+
): NormalizedRateLimitSnapshot | undefined {
|
|
841
|
+
const codexSnapshot = report.snapshots.find(isPrimaryCodexSnapshot);
|
|
842
|
+
if (!model || !isOpenAICodexModel(model)) return codexSnapshot ?? report.snapshots[0];
|
|
843
|
+
|
|
844
|
+
const modelKeys = normalizedModelUsageKeys(model);
|
|
845
|
+
const exactMatch = report.snapshots.find((snapshot) =>
|
|
846
|
+
normalizedSnapshotUsageKeys(snapshot).some((key) => modelKeys.has(key)),
|
|
847
|
+
);
|
|
848
|
+
if (exactMatch) return exactMatch;
|
|
849
|
+
|
|
850
|
+
const variants = codexModelVariantKeys(modelKeys);
|
|
851
|
+
for (const variant of variants) {
|
|
852
|
+
const matches = report.snapshots.filter(
|
|
853
|
+
(snapshot) =>
|
|
854
|
+
!isPrimaryCodexSnapshot(snapshot) &&
|
|
855
|
+
normalizedSnapshotUsageKeys(snapshot).some((key) => normalizedKeyHasToken(key, variant)),
|
|
856
|
+
);
|
|
857
|
+
if (matches.length === 1) return matches[0];
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return codexSnapshot ?? report.snapshots[0];
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function normalizedModelUsageKeys(model: CodexUsageModel): Set<string> {
|
|
864
|
+
const keys = new Set<string>();
|
|
865
|
+
addNormalizedUsageKey(keys, model.id);
|
|
866
|
+
addNormalizedUsageKey(keys, model.name);
|
|
867
|
+
|
|
868
|
+
for (const key of [...keys]) {
|
|
869
|
+
const codexIndex = key.indexOf("codex");
|
|
870
|
+
if (codexIndex >= 0) keys.add(key.slice(codexIndex));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return keys;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function normalizedSnapshotUsageKeys(snapshot: NormalizedRateLimitSnapshot): string[] {
|
|
877
|
+
return [normalizedUsageKey(snapshot.limitId), normalizedUsageKey(snapshot.limitName)].filter(
|
|
878
|
+
(key): key is string => key !== undefined,
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function addNormalizedUsageKey(keys: Set<string>, value: string | undefined): void {
|
|
883
|
+
const key = normalizedUsageKey(value);
|
|
884
|
+
if (key) keys.add(key);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function normalizedUsageKey(value: string | undefined): string | undefined {
|
|
888
|
+
const key = value
|
|
889
|
+
?.toLowerCase()
|
|
890
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
891
|
+
.replace(/^-+|-+$/g, "");
|
|
892
|
+
return key || undefined;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function codexModelVariantKeys(modelKeys: Set<string>): string[] {
|
|
896
|
+
const variants = new Set<string>();
|
|
897
|
+
for (const key of modelKeys) {
|
|
898
|
+
const match = key.match(/(?:^|-)codex-(.+)$/);
|
|
899
|
+
if (match?.[1]) variants.add(match[1]);
|
|
900
|
+
}
|
|
901
|
+
return [...variants];
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function normalizedKeyHasToken(key: string, token: string): boolean {
|
|
905
|
+
return (
|
|
906
|
+
key === token ||
|
|
907
|
+
key.startsWith(`${token}-`) ||
|
|
908
|
+
key.endsWith(`-${token}`) ||
|
|
909
|
+
key.includes(`-${token}-`)
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function formatStatuslinePrefix(snapshot: NormalizedRateLimitSnapshot): string {
|
|
914
|
+
if (isPrimaryCodexSnapshot(snapshot)) return "codex";
|
|
915
|
+
const label = snapshot.limitName ?? snapshot.limitId;
|
|
916
|
+
return `codex ${compactLimitLabel(label)}`;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function compactLimitLabel(label: string): string {
|
|
920
|
+
const normalized = label.replace(/[_-]+/g, " ").trim();
|
|
921
|
+
const codexVariant = normalized.match(/\bcodex\s+(.+)$/i)?.[1]?.trim();
|
|
922
|
+
const compact = codexVariant || normalized;
|
|
923
|
+
return compact.toLowerCase().replace(/\s+/g, " ");
|
|
924
|
+
}
|
|
925
|
+
|
|
822
926
|
function formatRemainingPercent(window: NormalizedRateLimitWindow): string {
|
|
823
927
|
return `${(100 - clampPercent(window.usedPercent)).toFixed(0)}%`;
|
|
824
928
|
}
|
|
@@ -840,7 +944,10 @@ function brightenInfoNotification(text: string): string {
|
|
|
840
944
|
}
|
|
841
945
|
|
|
842
946
|
function isPrimaryCodexSnapshot(snapshot: NormalizedRateLimitSnapshot): boolean {
|
|
843
|
-
return
|
|
947
|
+
return (
|
|
948
|
+
normalizedUsageKey(snapshot.limitId) === "codex" ||
|
|
949
|
+
normalizedUsageKey(snapshot.limitName) === "codex"
|
|
950
|
+
);
|
|
844
951
|
}
|
|
845
952
|
|
|
846
953
|
function formatWindowLine(label: string, window: NormalizedRateLimitWindow): string {
|