@monotykamary/pi-better-openai 0.1.35 → 0.1.37

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 CHANGED
@@ -29,6 +29,7 @@ Usage display, image generation, and live voice require pi's `openai-codex` OAut
29
29
 
30
30
  ## Features
31
31
 
32
+ - GPT-6 Astra and Daybreak Blue/Red model fallbacks for the built-in `openai-codex` provider.
32
33
  - Fast mode for supported OpenAI models, toggled with `/fast` or in `/openai-settings`.
33
34
  - OpenAI subscription usage display via `/openai-usage` and the footer.
34
35
  - Interactive settings picker via `/openai-settings`.
@@ -61,6 +62,7 @@ Default supported models:
61
62
  [
62
63
  "openai/gpt-5.4",
63
64
  "openai/gpt-5.5",
65
+ "openai-codex/gpt-6-astra",
64
66
  "openai-codex/gpt-5.6-sol",
65
67
  "openai-codex/gpt-5.6-terra",
66
68
  "openai-codex/gpt-5.6-luna",
@@ -111,6 +113,12 @@ Example config:
111
113
  }
112
114
  ```
113
115
 
116
+ ## Codex model fallbacks
117
+
118
+ The extension adds `gpt-6-astra`, `gpt-daybreak-blue-latest`, and `gpt-daybreak-red-latest` to the built-in `openai-codex` provider without requiring local `models.json` entries. Existing built-in models remain available, and metadata from pi's live catalog takes precedence when pi publishes an official entry with the same ID.
119
+
120
+ Daybreak models require separate OpenAI approval and provisioning. pi currently exposes reasoning levels through `max`; Codex's `ultra` automatic-delegation mode is not a pi thinking level.
121
+
114
122
  ## Live voice
115
123
 
116
124
  Run `/live` or press `Ctrl+Shift+L` to open the realtime voice panel. `Ctrl+L` remains pi's model selector, so the extension deliberately uses the shifted chord. While live mode has focus:
package/index.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  SettingsList,
13
13
  type SettingsListTheme,
14
14
  } from "@earendil-works/pi-tui";
15
+ import { registerOpenAICodexModels } from "./src/codex-models.ts";
15
16
  import { CONFIG_BASENAME, STATUS_KEY } from "./src/identity.ts";
16
17
  import {
17
18
  formatTokens,
@@ -209,6 +210,8 @@ function textPanel(title: string, lines: string[], done: () => void) {
209
210
  }
210
211
 
211
212
  export default function betterOpenAI(pi: ExtensionAPI): void {
213
+ registerOpenAICodexModels(pi);
214
+
212
215
  const fastController = new FastController(SERVICE_TIER);
213
216
  let cachedConfig: ResolvedConfig | undefined;
214
217
  let footerTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-better-openai",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "description": "Improve OpenAI in pi with fast mode, usage stats, realtime voice, image generation, and footer polish.",
5
5
  "keywords": [
6
6
  "fast",
@@ -62,6 +62,7 @@
62
62
  "ws": "8.21.2"
63
63
  },
64
64
  "devDependencies": {
65
+ "@earendil-works/pi-ai": "0.85.0",
65
66
  "@earendil-works/pi-coding-agent": "0.85.0",
66
67
  "@earendil-works/pi-tui": "0.85.0",
67
68
  "@types/node": "~22.19.0",
@@ -73,6 +74,7 @@
73
74
  "vitest": "^4.1.5"
74
75
  },
75
76
  "peerDependencies": {
77
+ "@earendil-works/pi-ai": "*",
76
78
  "@earendil-works/pi-coding-agent": "*",
77
79
  "@earendil-works/pi-tui": "*"
78
80
  },
@@ -0,0 +1,203 @@
1
+ import {
2
+ createProvider,
3
+ type Model,
4
+ type Provider,
5
+ type RefreshModelsContext,
6
+ } from "@earendil-works/pi-ai";
7
+ import { openAICodexResponsesApi } from "@earendil-works/pi-ai/api/openai-codex-responses.lazy";
8
+ import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
9
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+
11
+ const CODEX_API = "openai-codex-responses" as const;
12
+ const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
13
+ const CODEX_CATALOG_URL = "https://pi.dev/api/models/providers/openai-codex";
14
+ const CODEX_PROVIDER_ID = "openai-codex";
15
+ const CATALOG_REFRESH_INTERVAL_MS = 4 * 60 * 60 * 1000;
16
+
17
+ type CodexModel = Model<typeof CODEX_API>;
18
+
19
+ type CreateCodexProviderOptions = {
20
+ fetch?: typeof globalThis.fetch;
21
+ now?: () => number;
22
+ };
23
+
24
+ const CODEX_COMPAT = {
25
+ supportsOpenAIGrammarTools: true,
26
+ supportsAdditionalTools: true,
27
+ supportsToolSearch: true,
28
+ } as const;
29
+
30
+ export const CODEX_MODEL_FALLBACKS: CodexModel[] = [
31
+ {
32
+ id: "gpt-6-astra",
33
+ name: "GPT-6 Astra",
34
+ api: CODEX_API,
35
+ provider: CODEX_PROVIDER_ID,
36
+ baseUrl: CODEX_BASE_URL,
37
+ reasoning: true,
38
+ thinkingLevelMap: {
39
+ minimal: "low",
40
+ xhigh: "xhigh",
41
+ max: "max",
42
+ },
43
+ input: ["text", "image"],
44
+ contextWindow: 272000,
45
+ maxTokens: 128000,
46
+ cost: {
47
+ input: 10,
48
+ output: 50,
49
+ cacheRead: 1,
50
+ cacheWrite: 12.5,
51
+ tiers: [
52
+ {
53
+ inputTokensAbove: 272000,
54
+ input: 20,
55
+ output: 75,
56
+ cacheRead: 2,
57
+ cacheWrite: 25,
58
+ },
59
+ ],
60
+ },
61
+ compat: CODEX_COMPAT,
62
+ },
63
+ {
64
+ id: "gpt-daybreak-blue-latest",
65
+ name: "Daybreak Blue",
66
+ api: CODEX_API,
67
+ provider: CODEX_PROVIDER_ID,
68
+ baseUrl: CODEX_BASE_URL,
69
+ reasoning: true,
70
+ thinkingLevelMap: {
71
+ minimal: "low",
72
+ xhigh: "xhigh",
73
+ max: "max",
74
+ },
75
+ input: ["text", "image"],
76
+ contextWindow: 272000,
77
+ maxTokens: 128000,
78
+ cost: {
79
+ input: 4,
80
+ output: 20,
81
+ cacheRead: 0.4,
82
+ cacheWrite: 5,
83
+ tiers: [
84
+ {
85
+ inputTokensAbove: 272000,
86
+ input: 8,
87
+ output: 30,
88
+ cacheRead: 0.8,
89
+ cacheWrite: 10,
90
+ },
91
+ ],
92
+ },
93
+ compat: CODEX_COMPAT,
94
+ },
95
+ {
96
+ id: "gpt-daybreak-red-latest",
97
+ name: "Daybreak Red",
98
+ api: CODEX_API,
99
+ provider: CODEX_PROVIDER_ID,
100
+ baseUrl: CODEX_BASE_URL,
101
+ reasoning: true,
102
+ thinkingLevelMap: {
103
+ minimal: "low",
104
+ xhigh: "xhigh",
105
+ max: "max",
106
+ },
107
+ input: ["text", "image"],
108
+ contextWindow: 372000,
109
+ maxTokens: 128000,
110
+ cost: {
111
+ input: 12.5,
112
+ output: 75,
113
+ cacheRead: 1.25,
114
+ cacheWrite: 15.625,
115
+ },
116
+ compat: CODEX_COMPAT,
117
+ },
118
+ ];
119
+
120
+ function isRecord(value: unknown): value is Record<string, unknown> {
121
+ return typeof value === "object" && value !== null && !Array.isArray(value);
122
+ }
123
+
124
+ function catalogEntries(value: unknown): unknown[] | undefined {
125
+ if (Array.isArray(value)) return value;
126
+ if (!isRecord(value)) return undefined;
127
+ if (Array.isArray(value.models)) return value.models;
128
+ return Object.values(value);
129
+ }
130
+
131
+ export function parseCodexCatalog(value: unknown): CodexModel[] {
132
+ const entries = catalogEntries(value);
133
+ if (!entries) throw new Error("Invalid openai-codex model catalog");
134
+
135
+ return entries
136
+ .filter((entry): entry is Record<string, unknown> => {
137
+ return isRecord(entry) && typeof entry.id === "string" && entry.id.length > 0;
138
+ })
139
+ .map((entry) => ({ ...entry, provider: CODEX_PROVIDER_ID }) as CodexModel);
140
+ }
141
+
142
+ export function appendMissingCodexModels(
143
+ models: readonly CodexModel[],
144
+ fallbacks: readonly CodexModel[] = CODEX_MODEL_FALLBACKS,
145
+ ): CodexModel[] {
146
+ const merged = [...models];
147
+ const modelIds = new Set(merged.map((model) => model.id));
148
+ for (const model of fallbacks) {
149
+ if (modelIds.has(model.id)) continue;
150
+ merged.push(model);
151
+ modelIds.add(model.id);
152
+ }
153
+ return merged;
154
+ }
155
+
156
+ function storedCodexModels(context: RefreshModelsContext): CodexModel[] {
157
+ return (context.stored?.models ?? []).filter(
158
+ (model): model is CodexModel => model.provider === CODEX_PROVIDER_ID && model.api === CODEX_API,
159
+ );
160
+ }
161
+
162
+ async function fetchCodexModels(
163
+ context: RefreshModelsContext,
164
+ options: CreateCodexProviderOptions,
165
+ ): Promise<readonly CodexModel[]> {
166
+ const cached = storedCodexModels(context);
167
+ const checkedAt = context.stored?.checkedAt;
168
+ const now = options.now?.() ?? Date.now();
169
+ if (!context.force && checkedAt !== undefined && now - checkedAt < CATALOG_REFRESH_INTERVAL_MS) {
170
+ return cached;
171
+ }
172
+
173
+ const response = await (options.fetch ?? globalThis.fetch)(CODEX_CATALOG_URL, {
174
+ headers: { accept: "application/json" },
175
+ signal: context.signal,
176
+ });
177
+ if (response.status === 404 || response.status === 501) return cached;
178
+ if (!response.ok) {
179
+ throw new Error(`OpenAI Codex model catalog request failed: ${response.status}`);
180
+ }
181
+ return parseCodexCatalog(await response.json());
182
+ }
183
+
184
+ export function createOpenAICodexProvider(
185
+ options: CreateCodexProviderOptions = {},
186
+ ): Provider<typeof CODEX_API> {
187
+ const builtIn = openaiCodexProvider();
188
+ return createProvider({
189
+ id: builtIn.id,
190
+ name: builtIn.name,
191
+ baseUrl: builtIn.baseUrl,
192
+ headers: builtIn.headers,
193
+ auth: builtIn.auth,
194
+ models: appendMissingCodexModels(builtIn.getModels()),
195
+ fetchModels: (context) => fetchCodexModels(context, options),
196
+ filterModels: builtIn.filterModels,
197
+ api: openAICodexResponsesApi(),
198
+ });
199
+ }
200
+
201
+ export function registerOpenAICodexModels(pi: ExtensionAPI): void {
202
+ pi.registerProvider(createOpenAICodexProvider());
203
+ }
package/src/config.ts CHANGED
@@ -37,6 +37,7 @@ export const PET_STATES = [
37
37
  export const DEFAULT_SUPPORTED_MODELS = [
38
38
  "openai/gpt-5.4",
39
39
  "openai/gpt-5.5",
40
+ "openai-codex/gpt-6-astra",
40
41
  "openai-codex/gpt-5.6-sol",
41
42
  "openai-codex/gpt-5.6-terra",
42
43
  "openai-codex/gpt-5.6-luna",
package/src/usage.ts CHANGED
@@ -4,6 +4,7 @@ export { AUTH_FILE, readCodexAuth } from "./codex-auth.ts";
4
4
 
5
5
  export type UsageWindow = {
6
6
  used_percent?: number | null;
7
+ limit_window_seconds?: number | null;
7
8
  reset_after_seconds?: number | null;
8
9
  reset_at?: number | null;
9
10
  };
@@ -112,14 +113,14 @@ function formatResetClock(
112
113
  }
113
114
 
114
115
  function formatCompactReset(
115
- label: string,
116
+ label: string | undefined,
116
117
  seconds: number | null,
117
118
  options?: { includeDate?: boolean },
118
119
  now = Date.now(),
119
120
  ): string | null {
120
121
  const countdown = formatResetCountdown(seconds);
121
122
  const clock = formatResetClock(seconds, options, now);
122
- return countdown && clock ? `${label} ↺ ${countdown} - ${clock}` : null;
123
+ return countdown && clock ? `${label ? `${label} ` : ""}↺ ${countdown} - ${clock}` : null;
123
124
  }
124
125
 
125
126
  function isAbortSignal(value: unknown): value is AbortSignal {
@@ -202,6 +203,16 @@ export function usageScopeForModel(modelId: string | undefined): UsageScope {
202
203
  return modelId === SPARK_MODEL_ID ? "spark" : "default";
203
204
  }
204
205
 
206
+ function isWeeklyWindow(window: UsageWindow | null | undefined, now: number): boolean {
207
+ if (
208
+ typeof window?.limit_window_seconds === "number" &&
209
+ Number.isFinite(window.limit_window_seconds)
210
+ )
211
+ return window.limit_window_seconds >= 6 * 86_400;
212
+ const resetSeconds = getResetSeconds(window, now);
213
+ return resetSeconds !== null && resetSeconds > 5 * 3_600;
214
+ }
215
+
205
216
  export function parseUsageSnapshot(
206
217
  data: CodexUsageResponse,
207
218
  modelId: string | undefined,
@@ -212,13 +223,18 @@ export function parseUsageSnapshot(
212
223
  scope === "spark"
213
224
  ? (findSparkRateLimitBucket(data) ?? normalizeRateLimitBucket(data.rate_limit))
214
225
  : normalizeRateLimitBucket(data.rate_limit);
226
+ const primaryWindow = bucket?.primary_window;
227
+ const secondaryWindow = bucket?.secondary_window;
228
+ const weeklyOnly = !secondaryWindow && isWeeklyWindow(primaryWindow, now);
215
229
  return {
216
230
  capturedAt: now,
217
231
  scope,
218
- fiveHourLeftPercent: usedToLeftPercent(bucket?.primary_window?.used_percent),
219
- sevenDayLeftPercent: usedToLeftPercent(bucket?.secondary_window?.used_percent),
220
- fiveHourResetInSeconds: getResetSeconds(bucket?.primary_window, now),
221
- sevenDayResetInSeconds: getResetSeconds(bucket?.secondary_window, now),
232
+ fiveHourLeftPercent: weeklyOnly ? null : usedToLeftPercent(primaryWindow?.used_percent),
233
+ sevenDayLeftPercent: usedToLeftPercent(
234
+ weeklyOnly ? primaryWindow?.used_percent : secondaryWindow?.used_percent,
235
+ ),
236
+ fiveHourResetInSeconds: weeklyOnly ? null : getResetSeconds(primaryWindow, now),
237
+ sevenDayResetInSeconds: getResetSeconds(weeklyOnly ? primaryWindow : secondaryWindow, now),
222
238
  isLimited: bucket?.limit_reached === true || bucket?.allowed === false,
223
239
  };
224
240
  }
@@ -234,25 +250,40 @@ export function formatUsageSnapshot(
234
250
  options: { showResetTimes: boolean },
235
251
  now = Date.now(),
236
252
  ): string {
237
- const fiveHour = formatPercent(snapshot.fiveHourLeftPercent);
238
- const sevenDay = formatPercent(snapshot.sevenDayLeftPercent);
253
+ const windows = [
254
+ {
255
+ label: "5h",
256
+ percent: snapshot.fiveHourLeftPercent,
257
+ resetSeconds: snapshot.fiveHourResetInSeconds,
258
+ includeDate: false,
259
+ },
260
+ {
261
+ label: "7d",
262
+ percent: snapshot.sevenDayLeftPercent,
263
+ resetSeconds: snapshot.sevenDayResetInSeconds,
264
+ includeDate: true,
265
+ },
266
+ ];
267
+ const availableWindows = windows.filter(
268
+ (window) => window.percent !== null || window.resetSeconds !== null,
269
+ );
270
+ const displayedWindows = availableWindows.length > 0 ? availableWindows : windows;
271
+ const usage = displayedWindows
272
+ .map((window) => `${window.label}: ${formatPercent(window.percent)}`)
273
+ .join(" · ");
239
274
  const resets = options.showResetTimes
240
- ? [
241
- formatCompactReset(
242
- "5h",
243
- remainingResetSeconds(snapshot.fiveHourResetInSeconds, snapshot.capturedAt, now),
244
- undefined,
245
- now,
246
- ),
247
- formatCompactReset(
248
- "7d",
249
- remainingResetSeconds(snapshot.sevenDayResetInSeconds, snapshot.capturedAt, now),
250
- { includeDate: true },
251
- now,
252
- ),
253
- ].filter((value): value is string => value !== null)
275
+ ? displayedWindows
276
+ .map((window) =>
277
+ formatCompactReset(
278
+ displayedWindows.length > 1 ? window.label : undefined,
279
+ remainingResetSeconds(window.resetSeconds, snapshot.capturedAt, now),
280
+ window.includeDate ? { includeDate: true } : undefined,
281
+ now,
282
+ ),
283
+ )
284
+ .filter((value): value is string => value !== null)
254
285
  : [];
255
- return `Usage: 5h: ${fiveHour} · 7d: ${sevenDay}${resets.length ? ` · ${resets.join(" · ")}` : ""}`;
286
+ return `Usage: ${usage}${resets.length ? ` · ${resets.join(" · ")}` : ""}`;
256
287
  }
257
288
 
258
289
  function remainingResetSeconds(