@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/README.md +9 -3
- package/dist/chunks/{chunk-ACPQJ2CM.ts → chunk-N2ZD5X4K.ts} +4 -4
- package/dist/chunks/{chunk-ACPQJ2CM.ts.map → chunk-N2ZD5X4K.ts.map} +2 -2
- package/dist/chunks/{openai-companion-auth-FGFC6DBT.ts → openai-companion-auth-QIKCD3WA.ts} +2 -2
- package/dist/chunks/{openai-companion-usage-TGHK2OQR.ts → openai-companion-usage-ZFAHCGEY.ts} +2 -2
- package/dist/index.ts +372 -110
- package/dist/index.ts.map +4 -4
- package/package.json +1 -1
- package/src/codex-fast-catalog.ts +272 -0
- package/src/codex-fast-runtime.ts +107 -26
- package/src/codex-fast.ts +28 -19
- package/src/index.ts +3 -1
- package/src/query.ts +2 -1
- package/src/usage.ts +31 -10
- /package/dist/chunks/{openai-companion-auth-FGFC6DBT.ts.map → openai-companion-auth-QIKCD3WA.ts.map} +0 -0
- /package/dist/chunks/{openai-companion-usage-TGHK2OQR.ts.map → openai-companion-usage-ZFAHCGEY.ts.map} +0 -0
package/package.json
CHANGED
|
@@ -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: {
|
|
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
|
-
|
|
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
|
|
121
|
-
const
|
|
122
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
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(
|
|
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 (
|
|
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(
|
|
41
|
-
|
|
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 &&
|
|
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,
|