@narumitw/pi-usage 0.60.7 → 0.60.8
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 +91 -339
- package/dist/index.ts.map +2 -2
- package/docs/providers.md +2 -1
- package/package.json +73 -74
- package/src/codex-fast-runtime.ts +148 -184
- package/src/codex-fast.ts +74 -83
- package/src/codex-resets.ts +249 -278
- package/src/core.ts +204 -206
- package/src/format.ts +519 -574
- package/src/index.ts +77 -77
- package/src/oauth-credential-source.ts +57 -65
- package/src/providers/baseten.ts +32 -36
- package/src/providers/codex.ts +109 -120
- package/src/providers/deepseek.ts +59 -64
- package/src/providers/fireworks.ts +219 -238
- package/src/providers/github-copilot.ts +89 -95
- package/src/providers/kimi-coding.ts +202 -213
- package/src/providers/minimax.ts +214 -219
- package/src/providers/moonshot.ts +41 -46
- package/src/providers/opencode-zen.ts +56 -59
- package/src/providers/openrouter.ts +50 -53
- package/src/providers/vercel-ai-gateway.ts +33 -33
- package/src/providers/xai.ts +141 -148
- package/src/providers/zai-errors.ts +75 -75
- package/src/providers/zai.ts +173 -189
- package/src/query.ts +887 -967
- package/src/settings.ts +275 -304
- package/src/types.ts +145 -145
- package/src/usage-helpers.ts +18 -18
- package/src/usage-settings-ui.ts +117 -126
- package/src/usage-targets.ts +98 -114
- package/src/usage.ts +1264 -1426
package/src/codex-resets.ts
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { fingerprintResolvedAuth, sanitizeDisplayText } from "./core.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
type OAuthCredentialCandidateReader,
|
|
7
|
-
} from "./oauth-credential-source.js";
|
|
8
|
-
import {
|
|
9
|
-
AUTH_FINGERPRINT_SALT,
|
|
10
|
-
adapterForProvider,
|
|
11
|
-
fetchProviderJson,
|
|
12
|
-
resolveUsageAuth,
|
|
13
|
-
} from "./query.js";
|
|
4
|
+
import { fallbackOAuthCredentialCandidates, type OAuthCredentialCandidateReader } from "./oauth-credential-source.js";
|
|
5
|
+
import { AUTH_FINGERPRINT_SALT, adapterForProvider, fetchProviderJson, resolveUsageAuth } from "./query.js";
|
|
14
6
|
import type { ResolvedUsageAuth, UsageReport } from "./types.js";
|
|
15
7
|
|
|
16
8
|
const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
@@ -23,351 +15,330 @@ type StoredCredentialReader = (providerId: string) => unknown;
|
|
|
23
15
|
export type CodexResetOutcomeCode = "reset" | "nothing_to_reset" | "no_credit" | "already_redeemed";
|
|
24
16
|
|
|
25
17
|
export interface CodexResetOption {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
creditId?: string;
|
|
19
|
+
title: string;
|
|
20
|
+
description: string;
|
|
21
|
+
expiresAt?: number;
|
|
30
22
|
}
|
|
31
23
|
|
|
32
24
|
export interface CodexResetAvailability {
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
availableCount: number;
|
|
26
|
+
options: CodexResetOption[];
|
|
35
27
|
}
|
|
36
28
|
|
|
37
29
|
export interface CodexResetOutcome {
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
code: CodexResetOutcomeCode;
|
|
31
|
+
windowsReset: number;
|
|
40
32
|
}
|
|
41
33
|
|
|
42
34
|
export function codexResetCount(report: UsageReport): number | undefined {
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
const value = report.metrics.find((metric) => metric.id === "reset-credits")?.value;
|
|
36
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
45
37
|
}
|
|
46
38
|
|
|
47
39
|
export function codexResetActionDescription(report: UsageReport): string {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
const count = codexResetCount(report);
|
|
41
|
+
if (count === undefined) return "Check reset availability.";
|
|
42
|
+
if (count === 0) return "No usage limit resets available.";
|
|
43
|
+
return `You have ${count} ${resetLabel(count)} available.`;
|
|
52
44
|
}
|
|
53
45
|
|
|
54
46
|
export function genericCodexResetOption(): CodexResetOption {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
return {
|
|
48
|
+
title: "Full reset",
|
|
49
|
+
description: "Reset your current usage limits.",
|
|
50
|
+
};
|
|
59
51
|
}
|
|
60
52
|
|
|
61
53
|
export function resetOptionExpiration(option: CodexResetOption): string {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
if (option.expiresAt === undefined) return "Does not expire.";
|
|
55
|
+
const expiration = new Date(option.expiresAt * 1_000);
|
|
56
|
+
if (Number.isNaN(expiration.getTime())) return "Expiration unavailable.";
|
|
57
|
+
return `Expires ${expiration.toLocaleString()}.`;
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
export function resetConfirmationLines(option: CodexResetOption | undefined): string[] {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
61
|
+
if (!option) return ["The selected reset is unavailable."];
|
|
62
|
+
return [
|
|
63
|
+
option.title,
|
|
64
|
+
resetOptionExpiration(option),
|
|
65
|
+
option.description,
|
|
66
|
+
"This consumes one earned reset for the current OpenAI Codex account.",
|
|
67
|
+
];
|
|
76
68
|
}
|
|
77
69
|
|
|
78
70
|
export function formatCodexResetOutcome(
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
outcome: CodexResetOutcome | undefined,
|
|
72
|
+
remainingCount: number | undefined,
|
|
81
73
|
): string {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
return "No usage limit resets are available.";
|
|
74
|
+
const remaining =
|
|
75
|
+
remainingCount === undefined ? "" : ` You have ${remainingCount} ${resetLabel(remainingCount)} left.`;
|
|
76
|
+
if (!outcome) return "You don't have any usage limit resets available.";
|
|
77
|
+
if (outcome.code === "reset") return `Usage reset.${remaining}`.trim();
|
|
78
|
+
if (outcome.code === "already_redeemed") {
|
|
79
|
+
return `Usage reset was already completed.${remaining}`.trim();
|
|
80
|
+
}
|
|
81
|
+
if (outcome.code === "nothing_to_reset") {
|
|
82
|
+
return "Your usage does not need a reset right now.";
|
|
83
|
+
}
|
|
84
|
+
return "No usage limit resets are available.";
|
|
95
85
|
}
|
|
96
86
|
|
|
97
87
|
export function resetLabel(count: number): string {
|
|
98
|
-
|
|
88
|
+
return count === 1 ? "usage limit reset" : "usage limit resets";
|
|
99
89
|
}
|
|
100
90
|
|
|
101
91
|
export async function resolveCodexResetAuth(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
92
|
+
ctx: ExtensionContext,
|
|
93
|
+
salt: Uint8Array = AUTH_FINGERPRINT_SALT,
|
|
94
|
+
credentialReader: StoredCredentialReader = readStoredCredential,
|
|
95
|
+
candidateReader?: OAuthCredentialCandidateReader,
|
|
106
96
|
): Promise<ResolvedUsageAuth> {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
model: auth.model,
|
|
149
|
-
};
|
|
97
|
+
const model = ctx.model;
|
|
98
|
+
if (model?.provider !== "openai-codex") {
|
|
99
|
+
throw new Error("Usage limit resets require the current model to use OpenAI Codex.");
|
|
100
|
+
}
|
|
101
|
+
const expectedModel = `${model.provider}/${model.id}`;
|
|
102
|
+
const adapter = adapterForProvider("openai-codex");
|
|
103
|
+
if (!adapter) throw new Error("OpenAI Codex usage support is unavailable.");
|
|
104
|
+
const auth = await resolveUsageAuth(ctx, adapter, salt, credentialReader);
|
|
105
|
+
if (`${ctx.model?.provider}/${ctx.model?.id}` !== expectedModel) {
|
|
106
|
+
throw new Error("The current model changed while resolving Codex reset authentication.");
|
|
107
|
+
}
|
|
108
|
+
if (!auth) throw new Error("No runtime credential is configured for OpenAI Codex.");
|
|
109
|
+
|
|
110
|
+
const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
|
|
111
|
+
if (!resolvedAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
|
|
112
|
+
const resolvedAccountId = codexAccountIdFromAccessToken(resolvedAccess);
|
|
113
|
+
if (!resolvedAccountId) {
|
|
114
|
+
throw new Error("The active OpenAI Codex access token did not contain a valid account ID.");
|
|
115
|
+
}
|
|
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
|
+
const { accountId, storedAccess } = selectCodexResetCredential(
|
|
121
|
+
offered.candidates,
|
|
122
|
+
resolvedAccess,
|
|
123
|
+
resolvedAccountId,
|
|
124
|
+
offered.offeredCount === 0,
|
|
125
|
+
);
|
|
126
|
+
const authorization = `Bearer ${resolvedAccess}`;
|
|
127
|
+
const headers = {
|
|
128
|
+
Authorization: authorization,
|
|
129
|
+
"chatgpt-account-id": accountId,
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
apiKey: resolvedAccess,
|
|
133
|
+
headers,
|
|
134
|
+
fingerprint: fingerprintResolvedAuth({ headers }, salt),
|
|
135
|
+
secrets: [...new Set([...auth.secrets, storedAccess, resolvedAccess, authorization, accountId])],
|
|
136
|
+
model: auth.model,
|
|
137
|
+
};
|
|
150
138
|
}
|
|
151
139
|
|
|
152
140
|
export async function listCodexResetCredits(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
141
|
+
auth: ResolvedUsageAuth,
|
|
142
|
+
signal: AbortSignal,
|
|
143
|
+
timeoutMs: number,
|
|
156
144
|
): Promise<CodexResetAvailability> {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
145
|
+
const payload = await fetchProviderJson(
|
|
146
|
+
CODEX_RESET_CREDITS_URL,
|
|
147
|
+
auth,
|
|
148
|
+
signal,
|
|
149
|
+
timeoutMs,
|
|
150
|
+
"Codex usage-limit reset endpoint",
|
|
151
|
+
);
|
|
152
|
+
return normalizeCodexResetCreditsPayload(payload);
|
|
165
153
|
}
|
|
166
154
|
|
|
167
155
|
export async function consumeCodexResetCredit(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
156
|
+
auth: ResolvedUsageAuth,
|
|
157
|
+
option: CodexResetOption,
|
|
158
|
+
redeemRequestId: string,
|
|
159
|
+
signal: AbortSignal,
|
|
160
|
+
timeoutMs: number,
|
|
173
161
|
): Promise<CodexResetOutcome> {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return { code, windowsReset };
|
|
162
|
+
if (!redeemRequestId) throw new Error("Codex reset redemption request ID must not be empty.");
|
|
163
|
+
const payload = await fetchProviderJson(
|
|
164
|
+
CODEX_RESET_CONSUME_URL,
|
|
165
|
+
auth,
|
|
166
|
+
signal,
|
|
167
|
+
timeoutMs,
|
|
168
|
+
"Codex usage-limit reset consume endpoint",
|
|
169
|
+
{
|
|
170
|
+
method: "POST",
|
|
171
|
+
body: {
|
|
172
|
+
redeem_request_id: redeemRequestId,
|
|
173
|
+
...(option.creditId ? { credit_id: option.creditId } : {}),
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
const code = payload.code;
|
|
178
|
+
if (!isCodexResetOutcomeCode(code)) {
|
|
179
|
+
throw new Error("Codex reset consume endpoint returned an unknown outcome code.");
|
|
180
|
+
}
|
|
181
|
+
const windowsReset = payload.windows_reset === undefined ? 0 : nonnegativeInteger(payload.windows_reset);
|
|
182
|
+
if (windowsReset === undefined) {
|
|
183
|
+
throw new Error("Codex reset consume endpoint returned an invalid windows_reset value.");
|
|
184
|
+
}
|
|
185
|
+
return { code, windowsReset };
|
|
199
186
|
}
|
|
200
187
|
|
|
201
|
-
export function normalizeCodexResetCreditsPayload(
|
|
202
|
-
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (availableCount > 0 && options.length === 0) {
|
|
225
|
-
options.push(genericCodexResetOption());
|
|
226
|
-
}
|
|
227
|
-
return { availableCount, options };
|
|
188
|
+
export function normalizeCodexResetCreditsPayload(payload: Record<string, unknown>): CodexResetAvailability {
|
|
189
|
+
const availableCount = nonnegativeInteger(payload.available_count);
|
|
190
|
+
if (availableCount === undefined) {
|
|
191
|
+
throw new Error("Codex reset credits response returned an invalid available_count.");
|
|
192
|
+
}
|
|
193
|
+
const rawCredits = payload.credits;
|
|
194
|
+
if (rawCredits !== undefined && !Array.isArray(rawCredits)) {
|
|
195
|
+
throw new Error("Codex reset credits response returned invalid credits.");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const options = (rawCredits ?? [])
|
|
199
|
+
.map(asObject)
|
|
200
|
+
.filter((credit): credit is Record<string, unknown> => Boolean(credit))
|
|
201
|
+
.filter((credit) => credit.status === "available" && credit.reset_type === "codex_rate_limits")
|
|
202
|
+
.map(normalizeResetOption)
|
|
203
|
+
.sort((left, right) => (left.expiresAt ?? Number.MAX_SAFE_INTEGER) - (right.expiresAt ?? Number.MAX_SAFE_INTEGER))
|
|
204
|
+
.slice(0, Math.min(availableCount, MAX_RESET_OPTIONS));
|
|
205
|
+
|
|
206
|
+
if (availableCount > 0 && options.length === 0) {
|
|
207
|
+
options.push(genericCodexResetOption());
|
|
208
|
+
}
|
|
209
|
+
return { availableCount, options };
|
|
228
210
|
}
|
|
229
211
|
|
|
230
212
|
function selectCodexResetCredential(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
213
|
+
candidates: readonly unknown[],
|
|
214
|
+
resolvedAccess: string,
|
|
215
|
+
resolvedAccountId: string,
|
|
216
|
+
standaloneFallback: boolean,
|
|
235
217
|
): { accountId: string; storedAccess: string } {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
218
|
+
let sawOAuth = false;
|
|
219
|
+
let sawMatchingAccess = false;
|
|
220
|
+
let sawInvalidAccountId = false;
|
|
221
|
+
const matches = new Map<string, { accountId: string; storedAccess: string }>();
|
|
222
|
+
for (const candidate of candidates) {
|
|
223
|
+
try {
|
|
224
|
+
const credential = asObject(candidate);
|
|
225
|
+
if (credential?.type !== "oauth") continue;
|
|
226
|
+
sawOAuth = true;
|
|
227
|
+
const storedAccess = asNonemptyString(credential.access);
|
|
228
|
+
if (storedAccess !== resolvedAccess) continue;
|
|
229
|
+
sawMatchingAccess = true;
|
|
230
|
+
const accountId = validHeaderValue(credential.accountId);
|
|
231
|
+
const refresh = asNonemptyString(credential.refresh);
|
|
232
|
+
if (!accountId || accountId !== resolvedAccountId || !refresh) {
|
|
233
|
+
sawInvalidAccountId = true;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
matches.set(refresh, { accountId, storedAccess });
|
|
237
|
+
} catch {
|
|
238
|
+
// Malformed candidates never authorize a reset request.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (sawInvalidAccountId) {
|
|
242
|
+
throw new Error("The OpenAI Codex OAuth credential did not include a valid account ID.");
|
|
243
|
+
}
|
|
244
|
+
if (matches.size > 1) {
|
|
245
|
+
throw new Error("Conflicting OAuth credentials match the active OpenAI Codex runtime account.");
|
|
246
|
+
}
|
|
247
|
+
const match = matches.values().next().value;
|
|
248
|
+
if (match) return match;
|
|
249
|
+
if (!sawOAuth) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
standaloneFallback
|
|
252
|
+
? "Usage limit resets require the OpenAI Codex OAuth account configured through Pi /login."
|
|
253
|
+
: "Usage limit resets require an OpenAI Codex OAuth account configured through Pi /login or a compatible credential source.",
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (sawMatchingAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
|
|
257
|
+
throw new Error(
|
|
258
|
+
standaloneFallback
|
|
259
|
+
? "The active OpenAI Codex runtime account does not match Pi's stored OAuth account."
|
|
260
|
+
: "The active OpenAI Codex runtime account does not match any available OAuth account.",
|
|
261
|
+
);
|
|
280
262
|
}
|
|
281
263
|
|
|
282
264
|
function codexAccountIdFromAccessToken(access: string): string | undefined {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
265
|
+
try {
|
|
266
|
+
const parts = access.split(".");
|
|
267
|
+
if (parts.length !== 3 || !parts[1]) return undefined;
|
|
268
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as unknown;
|
|
269
|
+
const claims = asObject(asObject(payload)?.["https://api.openai.com/auth"]);
|
|
270
|
+
return validHeaderValue(claims?.chatgpt_account_id);
|
|
271
|
+
} catch {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
292
274
|
}
|
|
293
275
|
|
|
294
276
|
function normalizeResetOption(credit: Record<string, unknown>): CodexResetOption {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
277
|
+
const creditId = asOpaqueId(credit.id);
|
|
278
|
+
if (!creditId) throw new Error("Codex reset credits response returned an invalid credit ID.");
|
|
279
|
+
let expiresAt: number | undefined;
|
|
280
|
+
if (credit.expires_at !== undefined && credit.expires_at !== null) {
|
|
281
|
+
if (typeof credit.expires_at !== "string") {
|
|
282
|
+
throw new Error("Codex reset credits response returned an invalid expiration time.");
|
|
283
|
+
}
|
|
284
|
+
const parsed = Date.parse(credit.expires_at);
|
|
285
|
+
if (!Number.isFinite(parsed)) {
|
|
286
|
+
throw new Error("Codex reset credits response returned an invalid expiration time.");
|
|
287
|
+
}
|
|
288
|
+
expiresAt = Math.floor(parsed / 1_000);
|
|
289
|
+
}
|
|
290
|
+
const title = displayString(credit.title) ?? "Full reset";
|
|
291
|
+
const description = displayString(credit.description) ?? "Reset your current usage limits.";
|
|
292
|
+
return {
|
|
293
|
+
creditId,
|
|
294
|
+
title,
|
|
295
|
+
description,
|
|
296
|
+
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
297
|
+
};
|
|
316
298
|
}
|
|
317
299
|
|
|
318
300
|
function isCodexResetOutcomeCode(value: unknown): value is CodexResetOutcomeCode {
|
|
319
|
-
|
|
320
|
-
value === "reset" ||
|
|
321
|
-
value === "nothing_to_reset" ||
|
|
322
|
-
value === "no_credit" ||
|
|
323
|
-
value === "already_redeemed"
|
|
324
|
-
);
|
|
301
|
+
return value === "reset" || value === "nothing_to_reset" || value === "no_credit" || value === "already_redeemed";
|
|
325
302
|
}
|
|
326
303
|
|
|
327
304
|
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
328
|
-
|
|
329
|
-
|
|
305
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
306
|
+
return value as Record<string, unknown>;
|
|
330
307
|
}
|
|
331
308
|
|
|
332
309
|
function asNonemptyString(value: unknown): string | undefined {
|
|
333
|
-
|
|
310
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
334
311
|
}
|
|
335
312
|
|
|
336
313
|
function asOpaqueId(value: unknown): string | undefined {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
314
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_CREDIT_ID_CHARS) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
return value;
|
|
341
318
|
}
|
|
342
319
|
|
|
343
320
|
function displayString(value: unknown): string | undefined {
|
|
344
|
-
|
|
345
|
-
|
|
321
|
+
if (typeof value !== "string") return undefined;
|
|
322
|
+
return sanitizeDisplayText(value, 160) || undefined;
|
|
346
323
|
}
|
|
347
324
|
|
|
348
325
|
function validHeaderValue(value: unknown): string | undefined {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
326
|
+
if (typeof value !== "string" || !value || value.length > 512) return undefined;
|
|
327
|
+
if (/[^\x20-\x7e]/u.test(value)) return undefined;
|
|
328
|
+
return value;
|
|
352
329
|
}
|
|
353
330
|
|
|
354
331
|
function nonnegativeInteger(value: unknown): number | undefined {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
? Number(value)
|
|
360
|
-
: Number.NaN;
|
|
361
|
-
if (!Number.isSafeInteger(parsed) || parsed < 0) return undefined;
|
|
362
|
-
return parsed;
|
|
332
|
+
const parsed =
|
|
333
|
+
typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
|
|
334
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) return undefined;
|
|
335
|
+
return parsed;
|
|
363
336
|
}
|
|
364
337
|
|
|
365
338
|
function bearerToken(authorization: string | undefined): string | undefined {
|
|
366
|
-
|
|
339
|
+
return /^Bearer\s+(.+)$/iu.exec(authorization ?? "")?.[1];
|
|
367
340
|
}
|
|
368
341
|
|
|
369
342
|
function headerValue(headers: Record<string, string>, name: string): string | undefined {
|
|
370
|
-
|
|
371
|
-
([candidate]) => candidate.toLowerCase() === name.toLowerCase(),
|
|
372
|
-
)?.[1];
|
|
343
|
+
return Object.entries(headers).find(([candidate]) => candidate.toLowerCase() === name.toLowerCase())?.[1];
|
|
373
344
|
}
|