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