@monotykamary/pi-better-openai 0.2.2 → 0.2.4
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 +2 -2
- package/package.json +1 -1
- package/src/config.ts +39 -2
- package/src/reset-controller.ts +10 -1
- package/src/reset-guard.ts +33 -7
- package/src/resets.ts +10 -11
package/README.md
CHANGED
|
@@ -51,9 +51,9 @@ Usage display, image generation, and live voice require pi's `openai-codex` OAut
|
|
|
51
51
|
|
|
52
52
|
## Banked resets
|
|
53
53
|
|
|
54
|
-
Unused banked Codex resets **auto-redeem by default, 5 minutes before expiry**, while an interactive pi session is running and Codex credentials are available. Starting pi within that final five-minute window also triggers the check; expired credits are skipped. This runs independently of the usage display and current model. The reset picker
|
|
54
|
+
Unused banked Codex resets **auto-redeem by default, 5 minutes before expiry**, while an interactive pi session is running and Codex credentials are available. Starting pi within that final five-minute window also triggers the check; expired credits are skipped. This runs independently of the usage display and current model. The reset picker and confirmation show each credit's actual local auto-redemption date and time (expiry minus five minutes) beside its expiry. Disable **Auto-redeem banked resets** in `/openai-settings` or set `usage.autoRedeemBankedResets` to `false` to opt out.
|
|
55
55
|
|
|
56
|
-
For safety, each attempt targets one explicit, freshly checked credit ID, with no fallback to another credit and no automatic retry after a consume request (including errors or `nothing_to_reset`). A persistent per-account guard permits at most one redemption attempt in five minutes across pi sessions sharing the same agent directory; manual redemption uses the same guard. Simultaneously expiring credits are not drained. Reservations live under `$PI_CODING_AGENT_DIR/pi-better-openai/reset-redemptions` (default `~/.pi/agent/pi-better-openai/reset-redemptions`); unreadable state or an orphaned lock blocks redemption rather than risking a duplicate. Separate machines/agent directories cannot coordinate this local guard.
|
|
56
|
+
For safety, each attempt targets one explicit, freshly checked credit ID, with no fallback to another credit and no automatic retry after a consume request (including errors or `nothing_to_reset`). A persistent per-account guard permits at most one redemption attempt in five minutes across pi sessions sharing the same agent directory; manual redemption uses the same guard. Automatic reservations recheck the five-minute eligibility window while holding an exclusive filesystem lock, and attempted credit IDs remain blocked even after later redemptions or restarts. Simultaneously expiring credits are not drained, and later credits wait for their own final five-minute window. Reservations live under `$PI_CODING_AGENT_DIR/pi-better-openai/reset-redemptions` (default `~/.pi/agent/pi-better-openai/reset-redemptions`); unreadable state or an orphaned lock blocks redemption rather than risking a duplicate. Update/restart all pi instances to use the current guard. Separate machines/agent directories cannot coordinate this local guard; instances using the same account should share an agent directory.
|
|
57
57
|
|
|
58
58
|
Pi must remain running and awake; this is not an OS-level scheduled task. No eligible usage window or unavailable credentials can prevent redemption.
|
|
59
59
|
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -45,6 +45,31 @@ export const DEFAULT_SUPPORTED_MODELS = [
|
|
|
45
45
|
"openai-codex/gpt-5.5",
|
|
46
46
|
] as const;
|
|
47
47
|
|
|
48
|
+
// Earlier versions seeded config files with an explicit copy of the then-current
|
|
49
|
+
// default list. Treat those exact snapshots as unset so later additions to
|
|
50
|
+
// DEFAULT_SUPPORTED_MODELS (such as gpt-6-astra) reach existing installs.
|
|
51
|
+
const LEGACY_SEEDED_SUPPORTED_MODELS: readonly (readonly string[])[] = [
|
|
52
|
+
["openai/gpt-5.4", "openai/gpt-5.5", "openai-codex/gpt-5.4", "openai-codex/gpt-5.5"],
|
|
53
|
+
[
|
|
54
|
+
"openai/gpt-5.4",
|
|
55
|
+
"openai/gpt-5.5",
|
|
56
|
+
"openai/gpt-5.6-sol",
|
|
57
|
+
"openai/gpt-5.6-terra",
|
|
58
|
+
"openai/gpt-5.6-luna",
|
|
59
|
+
"openai-codex/gpt-5.4",
|
|
60
|
+
"openai-codex/gpt-5.5",
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"openai/gpt-5.4",
|
|
64
|
+
"openai/gpt-5.5",
|
|
65
|
+
"openai-codex/gpt-5.6-sol",
|
|
66
|
+
"openai-codex/gpt-5.6-terra",
|
|
67
|
+
"openai-codex/gpt-5.6-luna",
|
|
68
|
+
"openai-codex/gpt-5.4",
|
|
69
|
+
"openai-codex/gpt-5.5",
|
|
70
|
+
],
|
|
71
|
+
];
|
|
72
|
+
|
|
48
73
|
export type FooterMode = (typeof FOOTER_MODES)[number];
|
|
49
74
|
export type ImageSaveMode = (typeof IMAGE_SAVE_MODES)[number];
|
|
50
75
|
export type ImageOutputFormat = (typeof IMAGE_OUTPUT_FORMATS)[number];
|
|
@@ -194,11 +219,12 @@ export const DEFAULT_PET_CONFIG: Required<PetConfig> = {
|
|
|
194
219
|
};
|
|
195
220
|
|
|
196
221
|
export const DEFAULT_CONFIG: ConfigFile = {
|
|
222
|
+
// supportedModels is intentionally omitted so generated config files never pin
|
|
223
|
+
// the default list; resolution falls back to DEFAULT_SUPPORTED_MODELS live.
|
|
197
224
|
persistState: true,
|
|
198
225
|
notifyOnModelSwitch: true,
|
|
199
226
|
active: false,
|
|
200
227
|
desiredActive: false,
|
|
201
|
-
supportedModels: [...DEFAULT_SUPPORTED_MODELS],
|
|
202
228
|
usage: DEFAULT_USAGE_CONFIG,
|
|
203
229
|
footer: DEFAULT_FOOTER_CONFIG,
|
|
204
230
|
image: DEFAULT_IMAGE_CONFIG,
|
|
@@ -631,6 +657,14 @@ export function parseModels(value: unknown): SupportedModel[] | undefined {
|
|
|
631
657
|
.filter((entry): entry is SupportedModel => entry !== undefined);
|
|
632
658
|
}
|
|
633
659
|
|
|
660
|
+
function isLegacySeededSupportedModels(models: readonly SupportedModel[]): boolean {
|
|
661
|
+
return LEGACY_SEEDED_SUPPORTED_MODELS.some((snapshot) => {
|
|
662
|
+
if (snapshot.length !== models.length) return false;
|
|
663
|
+
const remaining = new Set(snapshot);
|
|
664
|
+
return models.every((model) => remaining.delete(`${model.provider}/${model.id}`));
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
634
668
|
export function readRawConfig(path: string): Record<string, unknown> {
|
|
635
669
|
if (!existsSync(path)) return {};
|
|
636
670
|
try {
|
|
@@ -823,6 +857,7 @@ export function resolveConfig(cwd: string): ResolvedConfig {
|
|
|
823
857
|
const merged = { ...DEFAULT_CONFIG, ...globalConfig, ...projectConfig };
|
|
824
858
|
const selectedPath = projectConfigExists ? paths.project : paths.global;
|
|
825
859
|
const desiredActive = merged.desiredActive ?? merged.active ?? false;
|
|
860
|
+
const configuredSupportedModels = parseModels(merged.supportedModels);
|
|
826
861
|
|
|
827
862
|
return {
|
|
828
863
|
configPath: selectedPath,
|
|
@@ -835,7 +870,9 @@ export function resolveConfig(cwd: string): ResolvedConfig {
|
|
|
835
870
|
active: merged.active ?? desiredActive,
|
|
836
871
|
desiredActive,
|
|
837
872
|
supportedModels:
|
|
838
|
-
|
|
873
|
+
configuredSupportedModels && !isLegacySeededSupportedModels(configuredSupportedModels)
|
|
874
|
+
? configuredSupportedModels
|
|
875
|
+
: (parseModels(DEFAULT_SUPPORTED_MODELS) ?? []),
|
|
839
876
|
usage: {
|
|
840
877
|
...DEFAULT_USAGE_CONFIG,
|
|
841
878
|
...globalConfig.usage,
|
package/src/reset-controller.ts
CHANGED
|
@@ -218,7 +218,16 @@ export class ResetController {
|
|
|
218
218
|
credit.expiresAtMs - Date.now() > BANKED_RESET_AUTO_REDEEM_LEAD_MS
|
|
219
219
|
)
|
|
220
220
|
return;
|
|
221
|
-
if (
|
|
221
|
+
if (
|
|
222
|
+
!reserveBankedResetRedemption(credentials.accountId, credit.id, {
|
|
223
|
+
expiresAtMs: credit.expiresAtMs,
|
|
224
|
+
})
|
|
225
|
+
)
|
|
226
|
+
return;
|
|
227
|
+
// Filesystem contention or a clock adjustment must not turn a valid
|
|
228
|
+
// reservation into a POST for an expired or not-yet-due credit.
|
|
229
|
+
const remainingMs = credit.expiresAtMs - Date.now();
|
|
230
|
+
if (remainingMs <= 0 || remainingMs > BANKED_RESET_AUTO_REDEEM_LEAD_MS) return;
|
|
222
231
|
const result = await consumeBankedReset(
|
|
223
232
|
ctx,
|
|
224
233
|
credit.id,
|
package/src/reset-guard.ts
CHANGED
|
@@ -4,6 +4,12 @@ import { join } from "node:path";
|
|
|
4
4
|
import { piAgentDir } from "./paths.ts";
|
|
5
5
|
import { BANKED_RESET_AUTO_REDEEM_LEAD_MS } from "./resets.ts";
|
|
6
6
|
|
|
7
|
+
type RedemptionReservation = {
|
|
8
|
+
blockedUntilMs: number;
|
|
9
|
+
creditHash: string;
|
|
10
|
+
attemptedCreditHashes?: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
7
13
|
// Reserve before sending a POST, not after receiving its outcome. A timeout may
|
|
8
14
|
// already have spent the credit. Shared by manual/automatic redemption and all
|
|
9
15
|
// pi processes using this agent directory. Corrupt state or an orphaned lock
|
|
@@ -11,8 +17,9 @@ import { BANKED_RESET_AUTO_REDEEM_LEAD_MS } from "./resets.ts";
|
|
|
11
17
|
export function reserveBankedResetRedemption(
|
|
12
18
|
accountId: string,
|
|
13
19
|
creditId: string,
|
|
14
|
-
|
|
20
|
+
options: { expiresAtMs?: number } = {},
|
|
15
21
|
): boolean {
|
|
22
|
+
if (!accountId.trim() || !creditId.trim()) return false;
|
|
16
23
|
const digest = (value: string) => createHash("sha256").update(value).digest("hex");
|
|
17
24
|
const dir = join(piAgentDir(), "pi-better-openai", "reset-redemptions");
|
|
18
25
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
@@ -26,33 +33,52 @@ export function reserveBankedResetRedemption(
|
|
|
26
33
|
throw new Error("Banked reset safety reservation unavailable.");
|
|
27
34
|
}
|
|
28
35
|
try {
|
|
29
|
-
let previous:
|
|
36
|
+
let previous: RedemptionReservation | undefined;
|
|
30
37
|
try {
|
|
31
38
|
previous = JSON.parse(readFileSync(path, "utf8"));
|
|
32
39
|
if (
|
|
33
40
|
!previous ||
|
|
34
41
|
typeof previous.blockedUntilMs !== "number" ||
|
|
35
42
|
!Number.isFinite(previous.blockedUntilMs) ||
|
|
36
|
-
typeof previous.creditHash !== "string"
|
|
43
|
+
typeof previous.creditHash !== "string" ||
|
|
44
|
+
!/^[0-9a-f]{64}$/.test(previous.creditHash) ||
|
|
45
|
+
(previous.attemptedCreditHashes !== undefined &&
|
|
46
|
+
(!Array.isArray(previous.attemptedCreditHashes) ||
|
|
47
|
+
!previous.attemptedCreditHashes.every(
|
|
48
|
+
(hash) => typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash),
|
|
49
|
+
) ||
|
|
50
|
+
!previous.attemptedCreditHashes.includes(previous.creditHash)))
|
|
37
51
|
)
|
|
38
52
|
throw new Error("Invalid reservation");
|
|
39
53
|
} catch (error) {
|
|
40
54
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT")
|
|
41
55
|
throw new Error("Banked reset safety reservation unavailable.");
|
|
42
56
|
}
|
|
43
|
-
|
|
57
|
+
// Check the actual clock under the account lock, not a cached timer's time.
|
|
58
|
+
// Manual, explicitly confirmed redemptions do not supply an expiry limit.
|
|
59
|
+
const now = Date.now();
|
|
44
60
|
if (
|
|
45
|
-
|
|
46
|
-
((
|
|
61
|
+
options.expiresAtMs !== undefined &&
|
|
62
|
+
(!Number.isFinite(options.expiresAtMs) ||
|
|
63
|
+
options.expiresAtMs <= now ||
|
|
64
|
+
options.expiresAtMs - now > BANKED_RESET_AUTO_REDEEM_LEAD_MS)
|
|
47
65
|
)
|
|
48
66
|
return false;
|
|
67
|
+
const creditHash = digest(creditId);
|
|
68
|
+
// Migrate old single-credit state. Never forget an uncertain attempt just
|
|
69
|
+
// because another credit was subsequently redeemed.
|
|
70
|
+
const attemptedCreditHashes =
|
|
71
|
+
previous?.attemptedCreditHashes ?? (previous ? [previous.creditHash] : []);
|
|
72
|
+
if ((previous && previous.blockedUntilMs > now) || attemptedCreditHashes.includes(creditHash))
|
|
73
|
+
return false;
|
|
49
74
|
writeFileSync(
|
|
50
75
|
path,
|
|
51
76
|
JSON.stringify({
|
|
52
77
|
blockedUntilMs: now + BANKED_RESET_AUTO_REDEEM_LEAD_MS,
|
|
53
78
|
creditHash,
|
|
79
|
+
attemptedCreditHashes: [...attemptedCreditHashes, creditHash],
|
|
54
80
|
}),
|
|
55
|
-
{ mode: 0o600 },
|
|
81
|
+
{ mode: 0o600, flush: true },
|
|
56
82
|
);
|
|
57
83
|
return true;
|
|
58
84
|
} finally {
|
package/src/resets.ts
CHANGED
|
@@ -156,6 +156,12 @@ function formatResetTimestamp(ms: number | null): string {
|
|
|
156
156
|
});
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
function formatAutoRedeemNote(credit: BankedResetCredit | undefined, enabled = false): string {
|
|
160
|
+
if (!enabled || !credit || !selectAutoRedeemCredit([credit]) || credit.expiresAtMs === null)
|
|
161
|
+
return "";
|
|
162
|
+
return ` · auto-redeems ${formatResetTimestamp(credit.expiresAtMs - BANKED_RESET_AUTO_REDEEM_LEAD_MS)}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
159
165
|
export function formatBankedResetChoice(
|
|
160
166
|
credit: BankedResetCredit,
|
|
161
167
|
index: number,
|
|
@@ -166,8 +172,7 @@ export function formatBankedResetChoice(
|
|
|
166
172
|
credit.expiresAtMs === null
|
|
167
173
|
? "no expiry"
|
|
168
174
|
: `expires ${formatResetTimestamp(credit.expiresAtMs)}`;
|
|
169
|
-
const note =
|
|
170
|
-
autoRedeem && selectAutoRedeemCredit([credit]) ? " · auto-redeems 5 min before expiry" : "";
|
|
175
|
+
const note = formatAutoRedeemNote(credit, autoRedeem);
|
|
171
176
|
return `${index + 1}. ${title} · ${expires}${note}`;
|
|
172
177
|
}
|
|
173
178
|
|
|
@@ -205,10 +210,7 @@ export function buildBankedResetConfirmation(options: {
|
|
|
205
210
|
lines.push("");
|
|
206
211
|
if (credit?.grantedAtMs != null)
|
|
207
212
|
lines.push(`Granted: ${formatResetTimestamp(credit.grantedAtMs)}`);
|
|
208
|
-
const autoNote =
|
|
209
|
-
options.autoRedeem && credit && selectAutoRedeemCredit([credit])
|
|
210
|
-
? " · auto-redeems 5 min before expiry"
|
|
211
|
-
: "";
|
|
213
|
+
const autoNote = formatAutoRedeemNote(credit, options.autoRedeem);
|
|
212
214
|
lines.push(`Expires: ${formatResetTimestamp(credit?.expiresAtMs ?? null)}${autoNote}`);
|
|
213
215
|
lines.push(`Available: ${options.availableCount}`);
|
|
214
216
|
const windows: string[] = [];
|
|
@@ -252,6 +254,7 @@ export async function consumeBankedReset(
|
|
|
252
254
|
signal?: AbortSignal,
|
|
253
255
|
pinnedCredentials?: CodexCredentials,
|
|
254
256
|
): Promise<ConsumeBankedResetResult> {
|
|
257
|
+
if (!creditId?.trim()) throw new Error("An explicit banked reset credit is required.");
|
|
255
258
|
const credentials = pinnedCredentials ?? (await getCodexCredentials(ctx, signal));
|
|
256
259
|
signal?.throwIfAborted();
|
|
257
260
|
if (!credentials)
|
|
@@ -264,11 +267,7 @@ export async function consumeBankedReset(
|
|
|
264
267
|
"chatgpt-account-id": credentials.accountId,
|
|
265
268
|
"content-type": "application/json",
|
|
266
269
|
},
|
|
267
|
-
body: JSON.stringify(
|
|
268
|
-
creditId
|
|
269
|
-
? { credit_id: creditId, redeem_request_id: redeemRequestId }
|
|
270
|
-
: { redeem_request_id: redeemRequestId },
|
|
271
|
-
),
|
|
270
|
+
body: JSON.stringify({ credit_id: creditId, redeem_request_id: redeemRequestId }),
|
|
272
271
|
signal,
|
|
273
272
|
});
|
|
274
273
|
if (!response.ok) throw new Error(`Codex reset consume request failed (${response.status})`);
|