pi-better-openai 0.1.2
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 +51 -0
- package/extensions/config.ts +232 -0
- package/extensions/format.ts +35 -0
- package/extensions/identity.ts +7 -0
- package/extensions/image.ts +427 -0
- package/extensions/pi-better-openai.ts +623 -0
- package/extensions/usage.ts +174 -0
- package/package.json +42 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export type UsageWindow = {
|
|
6
|
+
used_percent?: number | null;
|
|
7
|
+
reset_after_seconds?: number | null;
|
|
8
|
+
reset_at?: number | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type RateLimitBucket = {
|
|
12
|
+
allowed?: boolean;
|
|
13
|
+
limit_reached?: boolean;
|
|
14
|
+
primary_window?: UsageWindow | null;
|
|
15
|
+
secondary_window?: UsageWindow | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type CodexUsageResponse = {
|
|
19
|
+
rate_limit?: RateLimitBucket | null;
|
|
20
|
+
additional_rate_limits?: Record<string, unknown> | unknown[] | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type UsageSnapshot = {
|
|
24
|
+
fiveHourLeftPercent: number | null;
|
|
25
|
+
sevenDayLeftPercent: number | null;
|
|
26
|
+
fiveHourResetInSeconds: number | null;
|
|
27
|
+
sevenDayResetInSeconds: number | null;
|
|
28
|
+
isLimited: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const AGENT_DIR = process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent");
|
|
32
|
+
export const AUTH_FILE = join(AGENT_DIR, "auth.json");
|
|
33
|
+
export const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
34
|
+
const SPARK_MODEL_ID = "gpt-5.3-codex-spark";
|
|
35
|
+
const SPARK_LIMIT_NAME = "GPT-5.3-Codex-Spark";
|
|
36
|
+
|
|
37
|
+
function asObject(value: unknown): Record<string, unknown> | null {
|
|
38
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
39
|
+
return value as Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function clampPercent(value: number): number {
|
|
43
|
+
return Math.min(100, Math.max(0, value));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function usedToLeftPercent(value: number | null | undefined): number | null {
|
|
47
|
+
if (typeof value !== "number" || Number.isNaN(value)) return null;
|
|
48
|
+
return clampPercent(100 - value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatResetCountdown(seconds: number | null): string | null {
|
|
52
|
+
if (typeof seconds !== "number" || Number.isNaN(seconds)) return null;
|
|
53
|
+
const total = Math.max(0, Math.round(seconds));
|
|
54
|
+
const days = Math.floor(total / 86_400);
|
|
55
|
+
const hours = Math.floor((total % 86_400) / 3_600);
|
|
56
|
+
const minutes = Math.floor((total % 3_600) / 60);
|
|
57
|
+
const secs = total % 60;
|
|
58
|
+
if (days > 0) return `${days}d${hours}h`;
|
|
59
|
+
if (hours > 0) return `${hours}h${minutes}m`;
|
|
60
|
+
if (minutes > 0) return `${minutes}m`;
|
|
61
|
+
return `${secs}s`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatResetClock(seconds: number | null, options?: { includeDate?: boolean }): string | null {
|
|
65
|
+
if (typeof seconds !== "number" || Number.isNaN(seconds)) return null;
|
|
66
|
+
const resetDate = new Date(Date.now() + Math.max(0, seconds) * 1000);
|
|
67
|
+
const now = new Date();
|
|
68
|
+
const time = resetDate.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
|
|
69
|
+
if (!options?.includeDate && resetDate.toDateString() === now.toDateString()) return time;
|
|
70
|
+
const weekday = resetDate.toLocaleDateString(undefined, { weekday: "short" });
|
|
71
|
+
if (!options?.includeDate) return `${weekday} ${time}`;
|
|
72
|
+
const date = resetDate.toLocaleDateString(undefined, { month: "numeric", day: "numeric" });
|
|
73
|
+
return `${weekday} ${date} ${time}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatCompactReset(label: string, seconds: number | null, options?: { includeDate?: boolean }): string | null {
|
|
77
|
+
const countdown = formatResetCountdown(seconds);
|
|
78
|
+
const clock = formatResetClock(seconds, options);
|
|
79
|
+
return countdown && clock ? `${label} ↺ ${countdown} - ${clock}` : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function readCodexAuth(): { accessToken: string; accountId: string } | undefined {
|
|
83
|
+
try {
|
|
84
|
+
const auth = JSON.parse(readFileSync(AUTH_FILE, "utf8")) as Record<string, { type?: string; access?: string | null; accountId?: string | null; account_id?: string | null } | undefined>;
|
|
85
|
+
const entry = auth["openai-codex"];
|
|
86
|
+
if (entry?.type !== "oauth") return undefined;
|
|
87
|
+
const accessToken = entry.access?.trim();
|
|
88
|
+
const accountId = (entry.accountId ?? entry.account_id)?.trim();
|
|
89
|
+
return accessToken && accountId ? { accessToken, accountId } : undefined;
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function requestCodexUsage(signal?: AbortSignal): Promise<CodexUsageResponse | undefined> {
|
|
96
|
+
const credentials = readCodexAuth();
|
|
97
|
+
if (!credentials) return undefined;
|
|
98
|
+
const response = await fetch(USAGE_URL, {
|
|
99
|
+
headers: {
|
|
100
|
+
accept: "*/*",
|
|
101
|
+
authorization: `Bearer ${credentials.accessToken}`,
|
|
102
|
+
"chatgpt-account-id": credentials.accountId
|
|
103
|
+
},
|
|
104
|
+
signal
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) throw new Error(`Codex usage request failed (${response.status})`);
|
|
107
|
+
return (await response.json()) as CodexUsageResponse;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeRateLimitBucket(value: unknown): RateLimitBucket | null {
|
|
111
|
+
const record = asObject(value);
|
|
112
|
+
if (!record) return null;
|
|
113
|
+
if (!("primary_window" in record || "secondary_window" in record || "limit_reached" in record || "allowed" in record)) return null;
|
|
114
|
+
return record as RateLimitBucket;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function extractSparkRateLimitFromEntry(value: unknown): RateLimitBucket | null {
|
|
118
|
+
const record = asObject(value);
|
|
119
|
+
if (!record || record.limit_name !== SPARK_LIMIT_NAME) return null;
|
|
120
|
+
return normalizeRateLimitBucket(record.rate_limit);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function findSparkRateLimitBucket(data: CodexUsageResponse): RateLimitBucket | null {
|
|
124
|
+
const additional = data.additional_rate_limits;
|
|
125
|
+
if (Array.isArray(additional)) {
|
|
126
|
+
for (const entry of additional) {
|
|
127
|
+
const bucket = extractSparkRateLimitFromEntry(entry);
|
|
128
|
+
if (bucket) return bucket;
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
const map = asObject(additional);
|
|
132
|
+
if (map) {
|
|
133
|
+
for (const value of Object.values(map)) {
|
|
134
|
+
const bucket = extractSparkRateLimitFromEntry(value);
|
|
135
|
+
if (bucket) return bucket;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getResetSeconds(window: UsageWindow | null | undefined): number | null {
|
|
143
|
+
if (typeof window?.reset_after_seconds === "number" && !Number.isNaN(window.reset_after_seconds)) return window.reset_after_seconds;
|
|
144
|
+
if (typeof window?.reset_at !== "number" || Number.isNaN(window.reset_at)) return null;
|
|
145
|
+
const resetAtSeconds = window.reset_at > 100_000_000_000 ? window.reset_at / 1000 : window.reset_at;
|
|
146
|
+
return Math.max(0, resetAtSeconds - Date.now() / 1000);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function parseUsageSnapshot(data: CodexUsageResponse, modelId: string | undefined): UsageSnapshot {
|
|
150
|
+
const bucket = modelId === SPARK_MODEL_ID ? findSparkRateLimitBucket(data) : normalizeRateLimitBucket(data.rate_limit);
|
|
151
|
+
return {
|
|
152
|
+
fiveHourLeftPercent: usedToLeftPercent(bucket?.primary_window?.used_percent),
|
|
153
|
+
sevenDayLeftPercent: usedToLeftPercent(bucket?.secondary_window?.used_percent),
|
|
154
|
+
fiveHourResetInSeconds: getResetSeconds(bucket?.primary_window),
|
|
155
|
+
sevenDayResetInSeconds: getResetSeconds(bucket?.secondary_window),
|
|
156
|
+
isLimited: bucket?.limit_reached === true || bucket?.allowed === false
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function formatPercent(value: number | null): string {
|
|
161
|
+
return typeof value === "number" && !Number.isNaN(value) ? `${Math.round(clampPercent(value))}%` : "--";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function formatUsageSnapshot(snapshot: UsageSnapshot, options: { showResetTimes: boolean }): string {
|
|
165
|
+
const fiveHour = formatPercent(snapshot.fiveHourLeftPercent);
|
|
166
|
+
const sevenDay = formatPercent(snapshot.sevenDayLeftPercent);
|
|
167
|
+
const resets = options.showResetTimes
|
|
168
|
+
? [
|
|
169
|
+
formatCompactReset("5h", snapshot.fiveHourResetInSeconds),
|
|
170
|
+
formatCompactReset("7d", snapshot.sevenDayResetInSeconds, { includeDate: true })
|
|
171
|
+
].filter((value): value is string => value !== null)
|
|
172
|
+
: [];
|
|
173
|
+
return `Usage: 5h: ${fiveHour} | 7d: ${sevenDay}${resets.length ? ` | ${resets.join(" | ")}` : ""}`;
|
|
174
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-better-openai",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Personal pi extension that improves OpenAI with fast mode, usage stats, and footer polish.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fast",
|
|
7
|
+
"gpt",
|
|
8
|
+
"image",
|
|
9
|
+
"openai",
|
|
10
|
+
"pi",
|
|
11
|
+
"pi-extension",
|
|
12
|
+
"pi-package",
|
|
13
|
+
"priority",
|
|
14
|
+
"service-tier"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/mattleong/pi-better-openai.git"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"extensions/",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node tests/basic.mjs"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@mariozechner/pi-coding-agent": ">=0.57.0",
|
|
35
|
+
"@mariozechner/pi-tui": "*"
|
|
36
|
+
},
|
|
37
|
+
"pi": {
|
|
38
|
+
"extensions": [
|
|
39
|
+
"./extensions/pi-better-openai.ts"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
}
|