@narumitw/pi-codex-usage 0.14.1 → 0.15.1
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 +9 -2
- package/package.json +1 -1
- package/src/codex-usage.ts +11 -19
- package/src/format.ts +44 -36
- package/src/normalize.ts +108 -25
- package/src/types.ts +37 -0
package/README.md
CHANGED
|
@@ -9,8 +9,10 @@ Use it when you want a quick Codex-style usage summary without leaving Pi or req
|
|
|
9
9
|
## ✨ Features
|
|
10
10
|
|
|
11
11
|
- Adds a `/codex-status` command to Pi.
|
|
12
|
-
- Shows Codex
|
|
12
|
+
- Shows Codex usage windows, reset times, credits, and earned usage-limit resets.
|
|
13
|
+
- Labels each window from its reported duration (for example, 5-hour or weekly).
|
|
13
14
|
- Displays additional usage buckets when the Codex backend returns them.
|
|
15
|
+
- Reads the authoritative available reset count from the current Codex usage contract.
|
|
14
16
|
- Automatically shows a compact statusline item while the current Pi model uses `openai-codex`.
|
|
15
17
|
- Uses Pi's own OpenAI Codex subscription auth first.
|
|
16
18
|
- Falls back to `codex app-server --listen stdio://` only when Pi auth is unavailable.
|
|
@@ -59,6 +61,8 @@ information on rate limits and credits
|
|
|
59
61
|
GPT-5.3-Codex-Spark limit:
|
|
60
62
|
5h limit: [████████████████████] 100% left (resets 19:16)
|
|
61
63
|
Weekly limit: [████████████████████] 100% left (resets 00:10 on 21 May)
|
|
64
|
+
|
|
65
|
+
Usage limit resets: 2 available
|
|
62
66
|
```
|
|
63
67
|
|
|
64
68
|
## 📊 Statusline behavior
|
|
@@ -70,7 +74,7 @@ codex 59% 5h 61% wk
|
|
|
70
74
|
codex spark 100% 5h 100% wk
|
|
71
75
|
```
|
|
72
76
|
|
|
73
|
-
`@narumitw/pi-statusline` adds the default `📊` icon unless configured otherwise. The statusline value uses the cached usage snapshot and refreshes every five minutes while the current model remains `openai-codex`.
|
|
77
|
+
`@narumitw/pi-statusline` adds the default `📊` icon unless configured otherwise. The statusline value uses the cached usage snapshot and refreshes every five minutes while the current model remains `openai-codex`. Window labels come from the duration reported by Codex, with 5-hour/weekly fallbacks for older responses that omit it.
|
|
74
78
|
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
79
|
Switching away from an OpenAI Codex model clears the item.
|
|
76
80
|
|
|
@@ -85,6 +89,8 @@ Use `/codex-status --no-statusline` for a one-off notification without updating
|
|
|
85
89
|
|
|
86
90
|
This means Codex CLI is optional. Users who already use a Pi OpenAI Codex model or have logged in to Pi with ChatGPT Plus/Pro subscription auth can use the direct Pi-auth path.
|
|
87
91
|
|
|
92
|
+
The direct `/wham/usage` response can include the snake_case `rate_limit_reset_credits` summary. Current Codex app-server responses expose the same authoritative count as camelCase `rateLimitResetCredits` and may also include capped detail rows. The extension accepts both forms and keeps the compact statusline focused on rate-limit windows.
|
|
93
|
+
|
|
88
94
|
The extension does not read Pi or Codex auth files directly, and it does not expose bearer tokens in error messages.
|
|
89
95
|
|
|
90
96
|
## 🚧 Limitations
|
|
@@ -92,6 +98,7 @@ The extension does not read Pi or Codex auth files directly, and it does not exp
|
|
|
92
98
|
- OpenAI API keys are not ChatGPT Codex subscription auth and do not expose this quota.
|
|
93
99
|
- Usage data is a snapshot. Statusline and command results are cached for five minutes unless `--refresh` is used.
|
|
94
100
|
- The fallback path requires Codex CLI to be installed and logged in.
|
|
101
|
+
- `/codex-status` reports earned usage-limit resets but does not redeem them.
|
|
95
102
|
|
|
96
103
|
## 🗂️ Package layout
|
|
97
104
|
|
package/package.json
CHANGED
package/src/codex-usage.ts
CHANGED
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
ExtensionContext,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
formatCodexUsageReport,
|
|
8
|
-
formatCodexUsageStatusline,
|
|
9
|
-
formatQueryErrors,
|
|
10
|
-
showReport,
|
|
11
|
-
} from "./format.js";
|
|
12
|
-
import {
|
|
13
|
-
isOpenAICodexModel,
|
|
14
|
-
isStaleExtensionContextError,
|
|
15
|
-
queryUsage,
|
|
16
|
-
} from "./query.js";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { formatCodexUsageStatusline, formatQueryErrors, showReport } from "./format.js";
|
|
3
|
+
import { isOpenAICodexModel, isStaleExtensionContextError, queryUsage } from "./query.js";
|
|
17
4
|
import type {
|
|
18
5
|
CachedReport,
|
|
19
6
|
CodexUsageModel,
|
|
@@ -34,7 +21,11 @@ interface CommandArgumentCompletion {
|
|
|
34
21
|
|
|
35
22
|
const COMMAND_COMPLETIONS: readonly CommandArgumentCompletion[] = [
|
|
36
23
|
{ value: "--refresh", label: "--refresh", description: "Refresh usage instead of cached data" },
|
|
37
|
-
{
|
|
24
|
+
{
|
|
25
|
+
value: "--no-statusline",
|
|
26
|
+
label: "--no-statusline",
|
|
27
|
+
description: "Do not update the statusline",
|
|
28
|
+
},
|
|
38
29
|
{
|
|
39
30
|
value: "--clear-statusline",
|
|
40
31
|
label: "--clear-statusline",
|
|
@@ -192,8 +183,7 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
192
183
|
}
|
|
193
184
|
|
|
194
185
|
let keepStatusline = false;
|
|
195
|
-
const statuslineStarted =
|
|
196
|
-
options.value.statusline && setStatuslineValue(ctx, "checking");
|
|
186
|
+
const statuslineStarted = options.value.statusline && setStatuslineValue(ctx, "checking");
|
|
197
187
|
try {
|
|
198
188
|
const result = await queryUsage(ctx, options.value);
|
|
199
189
|
if (!result.ok) {
|
|
@@ -333,6 +323,8 @@ export type {
|
|
|
333
323
|
CodexUsageModel,
|
|
334
324
|
CodexUsageReport,
|
|
335
325
|
NormalizedCredits,
|
|
326
|
+
NormalizedRateLimitResetCredit,
|
|
327
|
+
NormalizedRateLimitResetCredits,
|
|
336
328
|
NormalizedRateLimitSnapshot,
|
|
337
329
|
NormalizedRateLimitWindow,
|
|
338
330
|
} from "./types.js";
|
package/src/format.ts
CHANGED
|
@@ -31,13 +31,20 @@ export function formatCodexUsageReport(report: CodexUsageReport, _cacheAgeMs?: n
|
|
|
31
31
|
if (!isPrimaryCodexSnapshot(snapshot)) {
|
|
32
32
|
lines.push(` ${label} limit:`);
|
|
33
33
|
}
|
|
34
|
-
if (snapshot.primary) lines.push(formatWindowLine("5h
|
|
35
|
-
if (snapshot.secondary) lines.push(formatWindowLine(
|
|
34
|
+
if (snapshot.primary) lines.push(formatWindowLine(snapshot.primary, "5h"));
|
|
35
|
+
if (snapshot.secondary) lines.push(formatWindowLine(snapshot.secondary, "weekly"));
|
|
36
36
|
if (!snapshot.primary && !snapshot.secondary) {
|
|
37
37
|
lines.push(" Limits unavailable for this account");
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
if (report.resetCredits) {
|
|
42
|
+
if (report.snapshots.length > 0) lines.push("");
|
|
43
|
+
lines.push(
|
|
44
|
+
` ${"Usage limit resets:".padEnd(LIMIT_VALUE_COLUMN)}${report.resetCredits.availableCount} available`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
return lines.join("\n");
|
|
42
49
|
}
|
|
43
50
|
|
|
@@ -49,8 +56,16 @@ export function formatCodexUsageStatusline(
|
|
|
49
56
|
if (!snapshot) return "usage unavailable";
|
|
50
57
|
|
|
51
58
|
const parts = [formatStatuslinePrefix(snapshot)];
|
|
52
|
-
if (snapshot.primary)
|
|
53
|
-
|
|
59
|
+
if (snapshot.primary) {
|
|
60
|
+
parts.push(
|
|
61
|
+
`${formatRemainingPercent(snapshot.primary)} ${formatWindowLabel(snapshot.primary, "5h", true)}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (snapshot.secondary) {
|
|
65
|
+
parts.push(
|
|
66
|
+
`${formatRemainingPercent(snapshot.secondary)} ${formatWindowLabel(snapshot.secondary, "weekly", true)}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
54
69
|
if (parts.length === 1 && snapshot.credits) parts.push(formatCredits(snapshot.credits));
|
|
55
70
|
return parts.join(" ");
|
|
56
71
|
}
|
|
@@ -171,10 +186,34 @@ function isPrimaryCodexSnapshot(snapshot: NormalizedRateLimitSnapshot): boolean
|
|
|
171
186
|
);
|
|
172
187
|
}
|
|
173
188
|
|
|
174
|
-
function formatWindowLine(
|
|
189
|
+
function formatWindowLine(
|
|
190
|
+
window: NormalizedRateLimitWindow,
|
|
191
|
+
fallback: "5h" | "weekly",
|
|
192
|
+
): string {
|
|
193
|
+
const label = `${formatWindowLabel(window, fallback, false)} limit:`;
|
|
175
194
|
return ` ${label.padEnd(LIMIT_VALUE_COLUMN)}${formatWindow(window)}`;
|
|
176
195
|
}
|
|
177
196
|
|
|
197
|
+
function formatWindowLabel(
|
|
198
|
+
window: NormalizedRateLimitWindow,
|
|
199
|
+
fallback: "5h" | "weekly",
|
|
200
|
+
compact: boolean,
|
|
201
|
+
): string {
|
|
202
|
+
const minutes = window.windowMinutes;
|
|
203
|
+
if (!minutes || !Number.isFinite(minutes) || minutes <= 0) {
|
|
204
|
+
return compact && fallback === "weekly" ? "wk" : capitalize(fallback);
|
|
205
|
+
}
|
|
206
|
+
if (minutes === 10_080) return compact ? "wk" : "Weekly";
|
|
207
|
+
if (minutes % 10_080 === 0) return `${minutes / 10_080}w`;
|
|
208
|
+
if (minutes % 1_440 === 0) return `${minutes / 1_440}d`;
|
|
209
|
+
if (minutes % 60 === 0) return `${minutes / 60}h`;
|
|
210
|
+
return `${minutes}m`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function capitalize(value: string): string {
|
|
214
|
+
return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
178
217
|
function formatWindow(window: NormalizedRateLimitWindow): string {
|
|
179
218
|
const remaining = 100 - clampPercent(window.usedPercent);
|
|
180
219
|
const reset = window.resetsAt ? ` (resets ${formatReset(window.resetsAt)})` : "";
|
|
@@ -222,37 +261,6 @@ export function formatQueryErrors(errors: UsageQueryError[]): string {
|
|
|
222
261
|
return lines.join("\n");
|
|
223
262
|
}
|
|
224
263
|
|
|
225
|
-
function formatPlanType(planType: string): string {
|
|
226
|
-
const key = planType
|
|
227
|
-
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
228
|
-
.toLowerCase()
|
|
229
|
-
.replace(/[^a-z0-9]+/g, "_");
|
|
230
|
-
if (key === "pro_lite" || key === "prolite") return "Pro Lite";
|
|
231
|
-
if (key === "team" || key === "self_serve_business_usage_based" || key === "business") {
|
|
232
|
-
return "Business";
|
|
233
|
-
}
|
|
234
|
-
if (key === "enterprise_cbp_usage_based") return "Enterprise";
|
|
235
|
-
|
|
236
|
-
const normalized = planType
|
|
237
|
-
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
238
|
-
.replace(/[_-]+/g, " ")
|
|
239
|
-
.trim();
|
|
240
|
-
if (!normalized) return planType;
|
|
241
|
-
return normalized
|
|
242
|
-
.split(/\s+/)
|
|
243
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
244
|
-
.join(" ");
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function formatDuration(milliseconds: number): string {
|
|
248
|
-
const seconds = Math.max(0, Math.round(milliseconds / 1000));
|
|
249
|
-
if (seconds < 60) return `${seconds}s`;
|
|
250
|
-
const minutes = Math.round(seconds / 60);
|
|
251
|
-
if (minutes < 60) return `${minutes}m`;
|
|
252
|
-
const hours = Math.round(minutes / 60);
|
|
253
|
-
return `${hours}h`;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
264
|
function formatNumber(value: number, fallback: string): string {
|
|
257
265
|
if (!Number.isFinite(value)) return fallback;
|
|
258
266
|
return new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value);
|
package/src/normalize.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AppServerCreditsSnapshot,
|
|
3
|
+
AppServerRateLimitResetCredit,
|
|
4
|
+
AppServerRateLimitResetCredits,
|
|
3
5
|
AppServerRateLimitResponse,
|
|
4
6
|
AppServerRateLimitSnapshot,
|
|
5
7
|
AppServerWindowSnapshot,
|
|
6
8
|
BackendAdditionalRateLimit,
|
|
7
9
|
BackendCreditsSnapshot,
|
|
8
10
|
BackendRateLimitDetails,
|
|
11
|
+
BackendRateLimitResetCredits,
|
|
9
12
|
BackendWindowSnapshot,
|
|
10
13
|
CodexUsageReport,
|
|
11
14
|
NormalizedCredits,
|
|
15
|
+
NormalizedRateLimitResetCredit,
|
|
16
|
+
NormalizedRateLimitResetCredits,
|
|
12
17
|
NormalizedRateLimitSnapshot,
|
|
13
18
|
NormalizedRateLimitWindow,
|
|
14
19
|
RateLimitStatusPayload,
|
|
@@ -29,27 +34,30 @@ export function normalizeBackendPayload(
|
|
|
29
34
|
? payload.additional_rate_limits
|
|
30
35
|
: [];
|
|
31
36
|
for (const item of additional) {
|
|
32
|
-
const additionalLimit =
|
|
33
|
-
|
|
34
|
-
"additional rate limit",
|
|
35
|
-
) as BackendAdditionalRateLimit;
|
|
37
|
+
const additionalLimit = asObject(item) as BackendAdditionalRateLimit | undefined;
|
|
38
|
+
if (!additionalLimit) continue;
|
|
36
39
|
const limitId =
|
|
37
40
|
asString(additionalLimit.metered_feature) ?? asString(additionalLimit.limit_name);
|
|
38
41
|
if (!limitId) continue;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
try {
|
|
43
|
+
const snapshot = normalizeBackendSnapshot(
|
|
44
|
+
limitId,
|
|
45
|
+
asString(additionalLimit.limit_name),
|
|
46
|
+
additionalLimit.rate_limit,
|
|
47
|
+
undefined,
|
|
48
|
+
);
|
|
49
|
+
if (snapshot) snapshots.push(snapshot);
|
|
50
|
+
} catch {
|
|
51
|
+
// Optional additional buckets must not hide otherwise usable primary/reset usage.
|
|
52
|
+
}
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
const resetCredits = normalizeBackendRateLimitResetCredits(payload.rate_limit_reset_credits);
|
|
56
|
+
if (snapshots.length === 0 && !resetCredits) {
|
|
57
|
+
throw new Error("Codex usage endpoint returned no displayable usage data.");
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
return { source, capturedAt, planType, snapshots };
|
|
60
|
+
return { source, capturedAt, planType, snapshots, resetCredits };
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
function normalizeBackendSnapshot(
|
|
@@ -95,13 +103,27 @@ function normalizeBackendCredits(value: unknown): NormalizedCredits | undefined
|
|
|
95
103
|
return { hasCredits, unlimited, balance: asString(credits.balance) };
|
|
96
104
|
}
|
|
97
105
|
|
|
106
|
+
function normalizeBackendRateLimitResetCredits(
|
|
107
|
+
value: unknown,
|
|
108
|
+
): NormalizedRateLimitResetCredits | undefined {
|
|
109
|
+
const resetCredits = asObject(value) as BackendRateLimitResetCredits | undefined;
|
|
110
|
+
const availableCount = asNonnegativeInteger(resetCredits?.available_count);
|
|
111
|
+
return availableCount === undefined ? undefined : { availableCount };
|
|
112
|
+
}
|
|
113
|
+
|
|
98
114
|
export function normalizeAppServerResponse(
|
|
99
115
|
response: AppServerRateLimitResponse,
|
|
100
116
|
capturedAt: number,
|
|
101
117
|
): CodexUsageReport {
|
|
102
118
|
const snapshots: NormalizedRateLimitSnapshot[] = [];
|
|
103
|
-
const addSnapshot = (raw: unknown, fallbackId: string) => {
|
|
104
|
-
|
|
119
|
+
const addSnapshot = (raw: unknown, fallbackId: string, optional = false) => {
|
|
120
|
+
let snapshot: NormalizedRateLimitSnapshot | undefined;
|
|
121
|
+
try {
|
|
122
|
+
snapshot = normalizeAppServerSnapshot(raw, fallbackId);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (optional) return;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
105
127
|
if (!snapshot) return;
|
|
106
128
|
const existingIndex = snapshots.findIndex((item) => item.limitId === snapshot.limitId);
|
|
107
129
|
if (existingIndex >= 0)
|
|
@@ -110,18 +132,26 @@ export function normalizeAppServerResponse(
|
|
|
110
132
|
};
|
|
111
133
|
|
|
112
134
|
addSnapshot(response.rateLimits, "codex");
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
const snapshotsByLimitId = asObject(response.rateLimitsByLimitId);
|
|
136
|
+
if (snapshotsByLimitId) {
|
|
137
|
+
for (const [limitId, raw] of Object.entries(snapshotsByLimitId)) {
|
|
138
|
+
if (limitId) addSnapshot(raw, limitId, true);
|
|
116
139
|
}
|
|
117
140
|
}
|
|
118
141
|
|
|
119
|
-
|
|
120
|
-
|
|
142
|
+
const resetCredits = normalizeAppServerRateLimitResetCredits(response.rateLimitResetCredits);
|
|
143
|
+
if (snapshots.length === 0 && !resetCredits) {
|
|
144
|
+
throw new Error("codex app-server returned no displayable usage data.");
|
|
121
145
|
}
|
|
122
146
|
|
|
123
147
|
const planType = asAppServerPlanType(response.rateLimits);
|
|
124
|
-
return {
|
|
148
|
+
return {
|
|
149
|
+
source: "codex-app-server",
|
|
150
|
+
capturedAt,
|
|
151
|
+
planType,
|
|
152
|
+
snapshots,
|
|
153
|
+
resetCredits,
|
|
154
|
+
};
|
|
125
155
|
}
|
|
126
156
|
|
|
127
157
|
function asAppServerPlanType(raw: unknown): string | undefined {
|
|
@@ -172,6 +202,49 @@ function normalizeAppServerCredits(value: unknown): NormalizedCredits | undefine
|
|
|
172
202
|
return { hasCredits, unlimited, balance: asString(credits.balance) };
|
|
173
203
|
}
|
|
174
204
|
|
|
205
|
+
function normalizeAppServerRateLimitResetCredits(
|
|
206
|
+
value: unknown,
|
|
207
|
+
): NormalizedRateLimitResetCredits | undefined {
|
|
208
|
+
const resetCredits = asObject(value) as AppServerRateLimitResetCredits | undefined;
|
|
209
|
+
const availableCount = asNonnegativeInteger(resetCredits?.availableCount);
|
|
210
|
+
if (availableCount === undefined) return undefined;
|
|
211
|
+
|
|
212
|
+
const rawCredits = resetCredits?.credits;
|
|
213
|
+
if (!Array.isArray(rawCredits)) return { availableCount };
|
|
214
|
+
|
|
215
|
+
const credits = rawCredits
|
|
216
|
+
.map(normalizeAppServerRateLimitResetCredit)
|
|
217
|
+
.filter((credit): credit is NormalizedRateLimitResetCredit => credit !== undefined)
|
|
218
|
+
.slice(0, availableCount);
|
|
219
|
+
if (rawCredits.length > 0 && credits.length === 0 && availableCount > 0) {
|
|
220
|
+
return { availableCount };
|
|
221
|
+
}
|
|
222
|
+
return { availableCount, credits };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function normalizeAppServerRateLimitResetCredit(
|
|
226
|
+
value: unknown,
|
|
227
|
+
): NormalizedRateLimitResetCredit | undefined {
|
|
228
|
+
const credit = asObject(value) as AppServerRateLimitResetCredit | undefined;
|
|
229
|
+
const id = asString(credit?.id);
|
|
230
|
+
if (!id) return undefined;
|
|
231
|
+
|
|
232
|
+
const normalized: NormalizedRateLimitResetCredit = { id };
|
|
233
|
+
const resetType = asString(credit?.resetType);
|
|
234
|
+
const status = asString(credit?.status);
|
|
235
|
+
const grantedAt = asNumber(credit?.grantedAt);
|
|
236
|
+
const expiresAt = asNumber(credit?.expiresAt);
|
|
237
|
+
const title = asString(credit?.title);
|
|
238
|
+
const description = asString(credit?.description);
|
|
239
|
+
if (resetType !== undefined) normalized.resetType = resetType;
|
|
240
|
+
if (status !== undefined) normalized.status = status;
|
|
241
|
+
if (grantedAt !== undefined) normalized.grantedAt = grantedAt;
|
|
242
|
+
if (expiresAt !== undefined) normalized.expiresAt = expiresAt;
|
|
243
|
+
if (title !== undefined) normalized.title = title;
|
|
244
|
+
if (description !== undefined) normalized.description = description;
|
|
245
|
+
return normalized;
|
|
246
|
+
}
|
|
247
|
+
|
|
175
248
|
function mergeSnapshot(
|
|
176
249
|
left: NormalizedRateLimitSnapshot,
|
|
177
250
|
right: NormalizedRateLimitSnapshot,
|
|
@@ -186,9 +259,13 @@ function mergeSnapshot(
|
|
|
186
259
|
}
|
|
187
260
|
|
|
188
261
|
function assertObject(value: unknown, description: string): Record<string, unknown> {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
262
|
+
const object = asObject(value);
|
|
263
|
+
if (!object) throw new Error(`${description} was not an object.`);
|
|
264
|
+
return object;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
268
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
192
269
|
return value as Record<string, unknown>;
|
|
193
270
|
}
|
|
194
271
|
|
|
@@ -208,3 +285,9 @@ function asNumber(value: unknown): number | undefined {
|
|
|
208
285
|
function asBoolean(value: unknown): boolean | undefined {
|
|
209
286
|
return typeof value === "boolean" ? value : undefined;
|
|
210
287
|
}
|
|
288
|
+
|
|
289
|
+
function asNonnegativeInteger(value: unknown): number | undefined {
|
|
290
|
+
const parsed = asNumber(value);
|
|
291
|
+
if (parsed === undefined || !Number.isSafeInteger(parsed)) return undefined;
|
|
292
|
+
return Math.max(0, parsed);
|
|
293
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -31,6 +31,22 @@ export type CodexUsageReport = {
|
|
|
31
31
|
capturedAt: number;
|
|
32
32
|
planType?: string;
|
|
33
33
|
snapshots: NormalizedRateLimitSnapshot[];
|
|
34
|
+
resetCredits?: NormalizedRateLimitResetCredits;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type NormalizedRateLimitResetCredits = {
|
|
38
|
+
availableCount: number;
|
|
39
|
+
credits?: NormalizedRateLimitResetCredit[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type NormalizedRateLimitResetCredit = {
|
|
43
|
+
id: string;
|
|
44
|
+
resetType?: string;
|
|
45
|
+
status?: string;
|
|
46
|
+
grantedAt?: number;
|
|
47
|
+
expiresAt?: number;
|
|
48
|
+
title?: string;
|
|
49
|
+
description?: string;
|
|
34
50
|
};
|
|
35
51
|
|
|
36
52
|
export type NormalizedRateLimitSnapshot = {
|
|
@@ -58,6 +74,11 @@ export type RateLimitStatusPayload = {
|
|
|
58
74
|
rate_limit?: unknown;
|
|
59
75
|
additional_rate_limits?: unknown;
|
|
60
76
|
credits?: unknown;
|
|
77
|
+
rate_limit_reset_credits?: unknown;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type BackendRateLimitResetCredits = {
|
|
81
|
+
available_count?: unknown;
|
|
61
82
|
};
|
|
62
83
|
|
|
63
84
|
export type BackendRateLimitDetails = {
|
|
@@ -86,6 +107,22 @@ export type BackendCreditsSnapshot = {
|
|
|
86
107
|
export type AppServerRateLimitResponse = {
|
|
87
108
|
rateLimits?: unknown;
|
|
88
109
|
rateLimitsByLimitId?: unknown;
|
|
110
|
+
rateLimitResetCredits?: unknown;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type AppServerRateLimitResetCredits = {
|
|
114
|
+
availableCount?: unknown;
|
|
115
|
+
credits?: unknown;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type AppServerRateLimitResetCredit = {
|
|
119
|
+
id?: unknown;
|
|
120
|
+
resetType?: unknown;
|
|
121
|
+
status?: unknown;
|
|
122
|
+
grantedAt?: unknown;
|
|
123
|
+
expiresAt?: unknown;
|
|
124
|
+
title?: unknown;
|
|
125
|
+
description?: unknown;
|
|
89
126
|
};
|
|
90
127
|
|
|
91
128
|
export type AppServerRateLimitSnapshot = {
|