@narumitw/pi-usage 0.64.3 → 0.65.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-usage",
3
- "version": "0.64.3",
3
+ "version": "0.65.0",
4
4
  "description": "Pi extension that shows current-account usage and DeepSeek API balance for supported providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,272 @@
1
+ import { type ExtensionContext, VERSION } from "@earendil-works/pi-coding-agent";
2
+ import { type CodexFastCapability, isOfficialCodexModel, UNKNOWN_CODEX_FAST_CAPABILITY } from "./codex-fast.js";
3
+ import { resolveCodexResetAuth } from "./codex-resets.js";
4
+ import { awaitWithDeadline, errorMessage, fingerprintResolvedAuth, redactUsageError } from "./core.js";
5
+ import type { OAuthCredentialCandidateReader, StoredCredentialReader } from "./oauth-credential-source.js";
6
+ import { AUTH_FINGERPRINT_SALT, fetchProviderJson } from "./query.js";
7
+ import type { PiModel, ResolvedUsageAuth } from "./types.js";
8
+ import { setBoundedMap } from "./usage-helpers.js";
9
+
10
+ const CACHE_TTL_MS = 5 * 60 * 1000;
11
+ const FAILURE_BACKOFF_MS = 30_000;
12
+ const MAX_ACCOUNT_STATES = 32;
13
+ const MAX_DIRECTORY_BODY_BYTES = 8 * 1024 * 1024;
14
+ const DIRECTORY_URL = new URL("https://chatgpt.com/backend-api/codex/models");
15
+ DIRECTORY_URL.searchParams.set("client_version", VERSION);
16
+
17
+ type CatalogEntry = {
18
+ models?: ReadonlyMap<string, boolean | undefined>;
19
+ fetchedAt: number;
20
+ fingerprint?: string;
21
+ retryAt?: number;
22
+ issue?: string;
23
+ };
24
+
25
+ type AccountScope = {
26
+ key: string;
27
+ fingerprint: string;
28
+ model: string;
29
+ session: string;
30
+ generation: number;
31
+ };
32
+
33
+ type CatalogOptions = {
34
+ credentialReader?: StoredCredentialReader;
35
+ candidateReader?: OAuthCredentialCandidateReader;
36
+ timeoutMs?: number;
37
+ onChanged?: (ctx: ExtensionContext) => void;
38
+ };
39
+
40
+ /** Account-scoped discovery; request hooks authenticate their cache read without awaiting directory I/O. */
41
+ export function createCodexFastCatalog(options: CatalogOptions = {}) {
42
+ const accounts = new Map<string, CatalogEntry>();
43
+ const requests = new Map<string, { controller: AbortController; promise: Promise<void> }>();
44
+ const latestRequests = new Map<string, number>();
45
+ const timeoutMs = options.timeoutMs ?? 15_000;
46
+ let requestSequence = 0;
47
+ let identitySequence = 0;
48
+ let generation = 0;
49
+ let active: AccountScope | undefined;
50
+ let issue: string = UNKNOWN_CODEX_FAST_CAPABILITY.reason;
51
+
52
+ const current = (scope: AccountScope, ctx: ExtensionContext, signal: AbortSignal) =>
53
+ !signal.aborted &&
54
+ generation === scope.generation &&
55
+ active?.key === scope.key &&
56
+ active.fingerprint === scope.fingerprint &&
57
+ active.model === scope.model &&
58
+ active.session === scope.session &&
59
+ modelIdentity(ctx.model) === scope.model &&
60
+ ctx.sessionManager.getSessionId() === scope.session;
61
+
62
+ const capability = (model: PiModel | undefined, key = active?.key): CodexFastCapability => {
63
+ const entry = key ? accounts.get(key) : undefined;
64
+ const supported = model ? entry?.models?.get(model.id) : undefined;
65
+ if (supported !== undefined) return { kind: supported ? "supported" : "unsupported" };
66
+ return {
67
+ kind: "unknown",
68
+ reason:
69
+ entry?.issue ??
70
+ (entry?.models && model
71
+ ? `The Codex model directory has no Fast capability information for ${model.id}.`
72
+ : issue),
73
+ };
74
+ };
75
+
76
+ const identify = async (ctx: ExtensionContext, signal: AbortSignal) => {
77
+ const identity = ++identitySequence;
78
+ const owner = generation;
79
+ const model = modelIdentity(ctx.model);
80
+ const session = ctx.sessionManager.getSessionId();
81
+ if (!isOfficialCodexModel(ctx.model)) {
82
+ active = undefined;
83
+ return undefined;
84
+ }
85
+ try {
86
+ // Reuse the existing active-runtime OAuth/account validation, without any reset request.
87
+ const auth = await awaitWithDeadline(
88
+ resolveCodexResetAuth(ctx, undefined, options.credentialReader, options.candidateReader),
89
+ signal,
90
+ timeoutMs,
91
+ "resolving the current Codex model-directory account",
92
+ );
93
+ if (
94
+ signal.aborted ||
95
+ owner !== generation ||
96
+ identity !== identitySequence ||
97
+ modelIdentity(ctx.model) !== model ||
98
+ ctx.sessionManager.getSessionId() !== session
99
+ ) {
100
+ return undefined;
101
+ }
102
+ const accountId = auth.headers["chatgpt-account-id"];
103
+ if (!accountId) throw new Error("The active Codex account ID is unavailable.");
104
+ const scope: AccountScope = {
105
+ key: fingerprintResolvedAuth({ headers: { "chatgpt-account-id": accountId } }, AUTH_FINGERPRINT_SALT),
106
+ fingerprint: auth.fingerprint,
107
+ model,
108
+ session,
109
+ generation: owner,
110
+ };
111
+ const changed = active?.key !== scope.key;
112
+ active = scope;
113
+ issue = "Codex Fast capability information has not been fetched for this account.";
114
+ if (changed) options.onChanged?.(ctx);
115
+ return { auth, scope };
116
+ } catch (error) {
117
+ if (signal.aborted || owner !== generation || identity !== identitySequence) return undefined;
118
+ active = undefined;
119
+ issue = `Codex Fast capability is unknown: ${redactUsageError(errorMessage(error))}`;
120
+ options.onChanged?.(ctx);
121
+ return undefined;
122
+ }
123
+ };
124
+
125
+ const load = async (
126
+ ctx: ExtensionContext,
127
+ auth: ResolvedUsageAuth,
128
+ scope: AccountScope,
129
+ signal: AbortSignal,
130
+ force: boolean,
131
+ ): Promise<void> => {
132
+ const entry = accounts.get(scope.key);
133
+ if (
134
+ !force &&
135
+ ((entry?.models && entry.fingerprint === auth.fingerprint && Date.now() - entry.fetchedAt < CACHE_TTL_MS) ||
136
+ (entry?.retryAt && Date.now() < entry.retryAt))
137
+ ) {
138
+ return;
139
+ }
140
+ const before = capability(ctx.model, scope.key);
141
+ const existing = requests.get(auth.fingerprint);
142
+ let promise = existing?.promise;
143
+ if (!promise) {
144
+ const sequence = ++requestSequence;
145
+ latestRequests.set(scope.key, sequence);
146
+ const controller = new AbortController();
147
+ const combined = AbortSignal.any([signal, controller.signal]);
148
+ let status: number | undefined;
149
+ promise = (async () => {
150
+ try {
151
+ const payload = await fetchProviderJson(
152
+ DIRECTORY_URL.href,
153
+ auth,
154
+ combined,
155
+ timeoutMs,
156
+ "Codex model directory",
157
+ {
158
+ redirect: "error",
159
+ maxSuccessBodyBytes: MAX_DIRECTORY_BODY_BYTES,
160
+ responseError: (responseStatus) => {
161
+ status = responseStatus;
162
+ return undefined;
163
+ },
164
+ },
165
+ );
166
+ const models = normalizeDirectory(payload);
167
+ if (combined.aborted || scope.generation !== generation || latestRequests.get(scope.key) !== sequence) return;
168
+ setBoundedMap(
169
+ accounts,
170
+ scope.key,
171
+ { models, fetchedAt: Date.now(), fingerprint: auth.fingerprint },
172
+ MAX_ACCOUNT_STATES,
173
+ );
174
+ } catch (error) {
175
+ if (combined.aborted || scope.generation !== generation || latestRequests.get(scope.key) !== sequence) return;
176
+ const previous = accounts.get(scope.key);
177
+ const transient = status === undefined || status >= 500 || status === 429;
178
+ setBoundedMap(
179
+ accounts,
180
+ scope.key,
181
+ {
182
+ ...(transient && previous?.models ? previous : { fetchedAt: 0 }),
183
+ retryAt: Date.now() + FAILURE_BACKOFF_MS,
184
+ issue: `Codex Fast capability is unknown: ${redactUsageError(errorMessage(error), auth.secrets)}`,
185
+ },
186
+ MAX_ACCOUNT_STATES,
187
+ );
188
+ } finally {
189
+ if (requests.get(auth.fingerprint)?.controller === controller) requests.delete(auth.fingerprint);
190
+ if (latestRequests.get(scope.key) === sequence) latestRequests.delete(scope.key);
191
+ }
192
+ })();
193
+ requests.set(auth.fingerprint, { controller, promise });
194
+ }
195
+ // A cancelled menu must stop waiting without aborting a session-owned shared request.
196
+ try {
197
+ await awaitWithDeadline(promise, signal, timeoutMs, "fetching the Codex model directory");
198
+ } catch (error) {
199
+ if (signal.aborted) return;
200
+ throw error;
201
+ }
202
+ const after = capability(ctx.model, scope.key);
203
+ if (current(scope, ctx, signal) && JSON.stringify(before) !== JSON.stringify(after)) options.onChanged?.(ctx);
204
+ };
205
+
206
+ return {
207
+ get(model: PiModel | undefined) {
208
+ return capability(model);
209
+ },
210
+ async read(
211
+ ctx: ExtensionContext,
212
+ signal: AbortSignal,
213
+ refresh = false,
214
+ force = false,
215
+ ): Promise<CodexFastCapability | undefined> {
216
+ const resolved = await identify(ctx, signal);
217
+ if (!resolved) return undefined;
218
+ const { auth, scope } = resolved;
219
+ if (refresh) await load(ctx, auth, scope, signal, force || capability(ctx.model, scope.key).kind === "unknown");
220
+ else void load(ctx, auth, scope, signal, false);
221
+ return current(scope, ctx, signal) ? capability(ctx.model, scope.key) : undefined;
222
+ },
223
+ reset(clearCache = false) {
224
+ generation += 1;
225
+ identitySequence += 1;
226
+ active = undefined;
227
+ issue = UNKNOWN_CODEX_FAST_CAPABILITY.reason;
228
+ for (const { controller } of requests.values()) controller.abort();
229
+ requests.clear();
230
+ latestRequests.clear();
231
+ if (clearCache) accounts.clear();
232
+ },
233
+ };
234
+ }
235
+
236
+ function normalizeDirectory(payload: Record<string, unknown>): ReadonlyMap<string, boolean | undefined> {
237
+ if (!Array.isArray(payload.models)) throw new Error("The Codex model directory returned invalid models.");
238
+ const models = new Map<string, boolean | undefined>();
239
+ for (const item of payload.models) {
240
+ if (!isRecord(item) || typeof item.slug !== "string" || !item.slug.trim() || models.has(item.slug)) {
241
+ throw new Error("The Codex model directory returned an invalid or duplicate model ID.");
242
+ }
243
+ const hasTiers = item.service_tiers !== undefined;
244
+ const hasLegacy = item.additional_speed_tiers !== undefined;
245
+ if (
246
+ (hasTiers &&
247
+ (!Array.isArray(item.service_tiers) ||
248
+ item.service_tiers.some((tier) => !isRecord(tier) || typeof tier.id !== "string"))) ||
249
+ (hasLegacy &&
250
+ (!Array.isArray(item.additional_speed_tiers) ||
251
+ item.additional_speed_tiers.some((tier) => typeof tier !== "string")))
252
+ ) {
253
+ throw new Error("The Codex model directory returned invalid service tiers.");
254
+ }
255
+ if (!hasTiers && !hasLegacy) {
256
+ models.set(item.slug, undefined);
257
+ continue;
258
+ }
259
+ const tiers = (item.service_tiers ?? []) as Array<{ id: string }>;
260
+ const legacy = (item.additional_speed_tiers ?? []) as string[];
261
+ models.set(item.slug, tiers.some((tier) => tier.id === "priority") || legacy.includes("fast"));
262
+ }
263
+ return models;
264
+ }
265
+
266
+ function modelIdentity(model: PiModel | undefined): string {
267
+ return model ? `${model.provider}/${model.id}` : "";
268
+ }
269
+
270
+ function isRecord(value: unknown): value is Record<string, unknown> {
271
+ return typeof value === "object" && value !== null && !Array.isArray(value);
272
+ }
@@ -1,18 +1,27 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
+ type CodexFastCapability,
3
4
  codexFastAvailability,
4
5
  codexFastIsEffective,
5
6
  codexFastStatusLabel,
6
7
  correctCodexFastMessageCost,
8
+ isOfficialCodexModel,
7
9
  rewriteCodexFastPayload,
10
+ UNKNOWN_CODEX_FAST_CAPABILITY,
8
11
  } from "./codex-fast.js";
12
+ import { createCodexFastCatalog } from "./codex-fast-catalog.js";
9
13
  import { errorMessage } from "./core.js";
14
+ import {
15
+ createOAuthCredentialCandidateReader,
16
+ type OAuthCredentialCandidateReader,
17
+ type StoredCredentialReader,
18
+ } from "./oauth-credential-source.js";
10
19
  import { isStaleExtensionContextError } from "./query.js";
11
20
  import type { UsageSettingsRuntime, UsageSettingsState } from "./settings.js";
12
21
  import type { PiModel } from "./types.js";
13
22
 
14
23
  const NO_FAST_REQUEST = Symbol("no-fast-request");
15
- type PendingFastRequest = { fastRequested: boolean; model: PiModel };
24
+ type PendingFastRequest = { fastRequested: boolean; model: PiModel; capability: CodexFastCapability };
16
25
 
17
26
  export const FAST_USAGE_WARNING = "Fast is about 1.5× faster and uses more of your plan allowance.";
18
27
 
@@ -20,11 +29,29 @@ export function registerCodexFastMode(
20
29
  pi: ExtensionAPI,
21
30
  settingsRuntime: UsageSettingsRuntime,
22
31
  refreshStatus: (ctx: ExtensionContext) => void,
23
- options: { registerSessionStart?: boolean } = {},
32
+ options: {
33
+ registerSessionStart?: boolean;
34
+ credentialReader?: StoredCredentialReader;
35
+ candidateReader?: OAuthCredentialCandidateReader;
36
+ timeoutMs?: number;
37
+ } = {},
24
38
  ) {
25
39
  let sessionController = new AbortController();
26
40
  let generation = 0;
27
41
  const pendingFastRequests = new Map<string, PendingFastRequest>();
42
+ const catalog = createCodexFastCatalog({
43
+ credentialReader: options.credentialReader,
44
+ candidateReader: options.candidateReader ?? createOAuthCredentialCandidateReader(pi, options.credentialReader),
45
+ timeoutMs: options.timeoutMs,
46
+ onChanged(ctx) {
47
+ if (sessionController.signal.aborted || !settingsRuntime.get().settings.codexFastMode) return;
48
+ try {
49
+ refreshStatus(ctx);
50
+ } catch (error) {
51
+ if (!isStaleExtensionContextError(error)) throw error;
52
+ }
53
+ },
54
+ });
28
55
 
29
56
  const toggle = async (
30
57
  ctx: ExtensionCommandContext,
@@ -34,6 +61,39 @@ export function registerCodexFastMode(
34
61
  const ownerGeneration = generation;
35
62
  const sessionId = ctx.sessionManager.getSessionId();
36
63
  const signal = callerSignal ? AbortSignal.any([callerSignal, sessionController.signal]) : sessionController.signal;
64
+ if (ctx.model?.provider !== "openai-codex") {
65
+ ctx.ui.notify("/fast is available only for the active OpenAI Codex model.", "warning");
66
+ return false;
67
+ }
68
+ if (!isOfficialCodexModel(ctx.model)) {
69
+ ctx.ui.notify("Fast mode requires the official OpenAI Codex Responses endpoint.", "warning");
70
+ return false;
71
+ }
72
+ if (settingsRuntime.get().kind === "invalid") {
73
+ ctx.ui.notify("pi-usage.json is invalid; repair it and run /reload before changing Fast mode.", "error");
74
+ return false;
75
+ }
76
+ if (enabled) {
77
+ const capability = await catalog.read(ctx, signal, true, catalog.get(ctx.model).kind !== "supported");
78
+ if (signal.aborted || ownerGeneration !== generation) return false;
79
+ if (!capability) {
80
+ const latest = catalog.get(ctx.model);
81
+ ctx.ui.notify(
82
+ latest.kind === "unknown"
83
+ ? latest.reason
84
+ : "The active Codex account changed while checking Fast capability.",
85
+ "warning",
86
+ );
87
+ return false;
88
+ }
89
+ const availability = codexFastAvailability(ctx.model, false, capability);
90
+ if (availability.kind !== "available") {
91
+ if (availability.kind === "unknown" || availability.kind === "unavailable") {
92
+ ctx.ui.notify(availability.reason, "warning");
93
+ }
94
+ return false;
95
+ }
96
+ }
37
97
  try {
38
98
  await settingsRuntime.update({ codexFastMode: enabled }, signal);
39
99
  } catch (error) {
@@ -63,27 +123,20 @@ export function registerCodexFastMode(
63
123
  return;
64
124
  }
65
125
  if (!ctx.hasUI) throw new Error("/fast requires TUI or RPC mode.");
66
- const availability = codexFastAvailability(ctx.model, settingsRuntime.get().settings.codexFastMode);
67
- if (availability.kind === "not-codex") {
68
- ctx.ui.notify("/fast is available only for the active OpenAI Codex model.", "warning");
69
- return;
70
- }
71
- if (availability.kind === "unavailable") {
72
- ctx.ui.notify(availability.reason, "warning");
73
- return;
74
- }
75
- if (settingsRuntime.get().kind === "invalid") {
76
- ctx.ui.notify("pi-usage.json is invalid; repair it and run /reload before changing Fast mode.", "error");
77
- return;
78
- }
79
- await toggle(ctx, !availability.enabled);
126
+ await toggle(ctx, !settingsRuntime.get().settings.codexFastMode);
80
127
  },
81
128
  });
82
129
 
130
+ const refreshCapabilities = (ctx: ExtensionContext, callerSignal?: AbortSignal, force = false) => {
131
+ const signal = callerSignal ? AbortSignal.any([callerSignal, sessionController.signal]) : sessionController.signal;
132
+ return catalog.read(ctx, signal, true, force);
133
+ };
134
+
83
135
  const prepareSession = (ctx: ExtensionContext): Promise<void> => {
84
136
  const sessionId = ctx.sessionManager.getSessionId();
85
137
  generation += 1;
86
138
  sessionController.abort();
139
+ catalog.reset();
87
140
  pendingFastRequests.clear();
88
141
  sessionController = new AbortController();
89
142
  const ownerGeneration = generation;
@@ -109,20 +162,33 @@ export function registerCodexFastMode(
109
162
  ctx.ui.notify(`Invalid pi-usage.json; using defaults without overwriting it. ${state.issue}`, "warning");
110
163
  }
111
164
  refreshStatus(ctx);
165
+ if (state.kind !== "invalid" && isOfficialCodexModel(ctx.model)) void refreshCapabilities(ctx);
112
166
  })();
113
167
  };
114
168
 
115
169
  if (options.registerSessionStart !== false) {
116
170
  pi.on("session_start", async (_event, ctx) => prepareSession(ctx));
117
171
  }
172
+ pi.on("model_select", (_event, ctx) => {
173
+ void refreshCapabilities(ctx);
174
+ });
118
175
 
119
- pi.on("before_provider_request", (event, ctx) => {
120
- const rewritten = rewriteCodexFastPayload(event.payload, ctx.model, settingsRuntime.get().settings.codexFastMode);
121
- const key = activeRequestKey(ctx);
122
- if (key && ctx.model) {
176
+ pi.on("before_provider_request", async (event, ctx) => {
177
+ const ownerGeneration = generation;
178
+ const model = ctx.model;
179
+ const enabled = settingsRuntime.get().settings.codexFastMode;
180
+ const capability =
181
+ enabled && isOfficialCodexModel(model)
182
+ ? ((await catalog.read(ctx, sessionController.signal)) ?? UNKNOWN_CODEX_FAST_CAPABILITY)
183
+ : UNKNOWN_CODEX_FAST_CAPABILITY;
184
+ if (sessionController.signal.aborted || ownerGeneration !== generation) return undefined;
185
+ const rewritten = rewriteCodexFastPayload(event.payload, model, enabled, capability);
186
+ const key = activeRequestKey(ctx, model);
187
+ if (key && model) {
123
188
  pendingFastRequests.set(key, {
124
189
  fastRequested: isRecord(rewritten) && rewritten.service_tier === "priority",
125
- model: ctx.model,
190
+ model,
191
+ capability,
126
192
  });
127
193
  }
128
194
  return rewritten;
@@ -130,30 +196,45 @@ export function registerCodexFastMode(
130
196
  pi.on("message_end", (event, ctx) => {
131
197
  const request = consumeFastRequest(ctx, event.message, pendingFastRequests);
132
198
  if (request === NO_FAST_REQUEST) return undefined;
133
- const message = correctCodexFastMessageCost(event.message, request.model, request.fastRequested);
199
+ const message = correctCodexFastMessageCost(
200
+ event.message,
201
+ request.model,
202
+ request.fastRequested,
203
+ request.capability,
204
+ );
134
205
  return message ? { message: message as never } : undefined;
135
206
  });
136
207
  pi.on("session_shutdown", async () => {
137
208
  generation += 1;
138
209
  sessionController.abort();
210
+ catalog.reset(true);
139
211
  pendingFastRequests.clear();
140
212
  await settingsRuntime.flush();
141
213
  });
142
214
 
143
215
  return {
144
216
  prepareSession,
217
+ refreshCapabilities,
145
218
  availability(model: PiModel | undefined) {
146
- return codexFastAvailability(model, settingsRuntime.get().settings.codexFastMode);
219
+ return codexFastAvailability(model, settingsRuntime.get().settings.codexFastMode, catalog.get(model));
147
220
  },
148
221
  decorateStatus(model: PiModel | undefined, status: string) {
149
- return codexFastStatusLabel(status, codexFastIsEffective(model, settingsRuntime.get().settings.codexFastMode));
222
+ const enabled = settingsRuntime.get().settings.codexFastMode;
223
+ const capability = catalog.get(model);
224
+ if (
225
+ enabled &&
226
+ codexFastAvailability(model, enabled, capability).kind === "unknown" &&
227
+ /^codex(?:\s|$)/u.test(status)
228
+ ) {
229
+ return `codex (Fast unknown)${status.slice("codex".length)}`;
230
+ }
231
+ return codexFastStatusLabel(status, codexFastIsEffective(model, enabled, capability));
150
232
  },
151
233
  toggle,
152
234
  };
153
235
  }
154
236
 
155
- function activeRequestKey(ctx: ExtensionContext): string | undefined {
156
- const model = ctx.model;
237
+ function activeRequestKey(ctx: ExtensionContext, model = ctx.model): string | undefined {
157
238
  return model ? `${ctx.sessionManager.getSessionId()}:${model.provider}/${model.id}` : undefined;
158
239
  }
159
240
 
package/src/codex-fast.ts CHANGED
@@ -4,23 +4,24 @@ import type { PiModel } from "./types.js";
4
4
  export const CODEX_FAST_SERVICE_TIER = "priority";
5
5
  export const CODEX_STANDARD_SERVICE_TIER = "default";
6
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
- "gpt-6-astra",
13
- "gpt-6-luna",
14
- "gpt-6-sol",
15
- "gpt-6.1-sol",
16
- ]);
7
+ export type CodexFastCapability = { kind: "supported" } | { kind: "unsupported" } | { kind: "unknown"; reason: string };
8
+
9
+ export const UNKNOWN_CODEX_FAST_CAPABILITY = {
10
+ kind: "unknown",
11
+ reason: "Codex Fast capability information has not been fetched.",
12
+ } as const;
17
13
 
18
14
  export type CodexFastAvailability =
19
15
  | { kind: "available"; enabled: boolean }
20
16
  | { kind: "not-codex" }
21
- | { kind: "unavailable"; reason: string };
17
+ | { kind: "unavailable"; reason: string }
18
+ | { kind: "unknown"; reason: string };
22
19
 
23
- export function codexFastAvailability(model: PiModel | undefined, enabled: boolean): CodexFastAvailability {
20
+ export function codexFastAvailability(
21
+ model: PiModel | undefined,
22
+ enabled: boolean,
23
+ capability: CodexFastCapability = UNKNOWN_CODEX_FAST_CAPABILITY,
24
+ ): CodexFastAvailability {
24
25
  if (model?.provider !== "openai-codex") return { kind: "not-codex" };
25
26
  if (!isOfficialCodexModel(model)) {
26
27
  return {
@@ -28,7 +29,8 @@ export function codexFastAvailability(model: PiModel | undefined, enabled: boole
28
29
  reason: "Fast mode requires the official OpenAI Codex Responses endpoint.",
29
30
  };
30
31
  }
31
- if (!CODEX_FAST_MODEL_IDS.has(model.id)) {
32
+ if (capability.kind === "unknown") return capability;
33
+ if (capability.kind === "unsupported") {
32
34
  return {
33
35
  kind: "unavailable",
34
36
  reason: `${model.id} does not advertise Codex Fast support.`,
@@ -37,24 +39,30 @@ export function codexFastAvailability(model: PiModel | undefined, enabled: boole
37
39
  return { kind: "available", enabled };
38
40
  }
39
41
 
40
- export function codexFastIsEffective(model: PiModel | undefined, enabled: boolean): boolean {
41
- return codexFastAvailability(model, enabled).kind === "available" && enabled;
42
+ export function codexFastIsEffective(
43
+ model: PiModel | undefined,
44
+ enabled: boolean,
45
+ capability: CodexFastCapability = UNKNOWN_CODEX_FAST_CAPABILITY,
46
+ ): boolean {
47
+ return codexFastAvailability(model, enabled, capability).kind === "available" && enabled;
42
48
  }
43
49
 
44
50
  export function codexFastRequestTier(
45
51
  model: PiModel | undefined,
46
52
  enabled: boolean,
53
+ capability: CodexFastCapability = UNKNOWN_CODEX_FAST_CAPABILITY,
47
54
  ): typeof CODEX_FAST_SERVICE_TIER | typeof CODEX_STANDARD_SERVICE_TIER | undefined {
48
55
  if (!isOfficialCodexModel(model)) return undefined;
49
- return enabled && CODEX_FAST_MODEL_IDS.has(model.id) ? CODEX_FAST_SERVICE_TIER : CODEX_STANDARD_SERVICE_TIER;
56
+ return enabled && capability.kind === "supported" ? CODEX_FAST_SERVICE_TIER : CODEX_STANDARD_SERVICE_TIER;
50
57
  }
51
58
 
52
59
  export function rewriteCodexFastPayload(
53
60
  payload: unknown,
54
61
  model: PiModel | undefined,
55
62
  enabled: boolean,
63
+ capability: CodexFastCapability = UNKNOWN_CODEX_FAST_CAPABILITY,
56
64
  ): unknown | undefined {
57
- const serviceTier = codexFastRequestTier(model, enabled);
65
+ const serviceTier = codexFastRequestTier(model, enabled, capability);
58
66
  if (!serviceTier || !isRecord(payload)) return undefined;
59
67
  return { ...payload, service_tier: serviceTier };
60
68
  }
@@ -63,9 +71,10 @@ export function correctCodexFastMessageCost(
63
71
  message: unknown,
64
72
  model: PiModel | undefined,
65
73
  fastRequested: boolean,
74
+ capability: CodexFastCapability = UNKNOWN_CODEX_FAST_CAPABILITY,
66
75
  ): unknown | undefined {
67
76
  if (
68
- !codexFastIsEffective(model, fastRequested) ||
77
+ !codexFastIsEffective(model, fastRequested, capability) ||
69
78
  !isRecord(message) ||
70
79
  message.role !== "assistant" ||
71
80
  message.provider !== model?.provider ||
@@ -92,7 +101,7 @@ export function codexFastStatusLabel(status: string, enabled: boolean): string {
92
101
  return status === "codex" ? "codex fast" : `codex fast${status.slice("codex".length)}`;
93
102
  }
94
103
 
95
- function isOfficialCodexModel(model: PiModel | undefined): model is PiModel & { api: "openai-codex-responses" } {
104
+ export function isOfficialCodexModel(model: PiModel | undefined): model is PiModel & { api: "openai-codex-responses" } {
96
105
  if (model?.provider !== "openai-codex" || !hasApi(model, "openai-codex-responses")) {
97
106
  return false;
98
107
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
+ export type { CodexFastAvailability, CodexFastCapability } from "./codex-fast.js";
1
2
  export {
2
- CODEX_FAST_MODEL_IDS,
3
3
  CODEX_FAST_SERVICE_TIER,
4
4
  CODEX_STANDARD_SERVICE_TIER,
5
5
  codexFastAvailability,
@@ -7,7 +7,9 @@ export {
7
7
  codexFastRequestTier,
8
8
  codexFastStatusLabel,
9
9
  correctCodexFastMessageCost,
10
+ isOfficialCodexModel,
10
11
  rewriteCodexFastPayload,
12
+ UNKNOWN_CODEX_FAST_CAPABILITY,
11
13
  } from "./codex-fast.js";
12
14
  export type {
13
15
  CodexResetAvailability,
package/src/query.ts CHANGED
@@ -679,6 +679,7 @@ export async function fetchProviderJson(
679
679
  body?: Record<string, unknown>;
680
680
  redirect?: RequestRedirect;
681
681
  userAgent?: boolean;
682
+ maxSuccessBodyBytes?: number;
682
683
  responseError?: (status: number, text: string) => string | undefined;
683
684
  } = {},
684
685
  ): Promise<Record<string, unknown>> {
@@ -710,7 +711,7 @@ export async function fetchProviderJson(
710
711
  if (controller.signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
711
712
  const text = await readBoundedResponse(
712
713
  response,
713
- response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
714
+ response.ok ? (request.maxSuccessBodyBytes ?? MAX_SUCCESS_BODY_BYTES) : MAX_ERROR_BODY_BYTES,
714
715
  !response.ok,
715
716
  description,
716
717
  controller.signal,