fix(codex): keep fast mode durable without prompt preflight #10
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@eleith-pi/codex": patch
|
||||
---
|
||||
|
||||
Keep saved fast-mode choices active without expiry across reloads, tree navigation, and model reselection. Check eligibility only when explicitly enabling fast mode, avoiding fast-mode authentication and catalog preparation on prompts while preserving scope checks and requested-versus-reported tier status.
|
||||
@@ -21,40 +21,11 @@ pi install npm:@eleith-pi/codex
|
||||
- `/eleith:codex context on` selects the verified local window when larger.
|
||||
- `/eleith:codex context off` returns to the catalog baseline, confirming reductions.
|
||||
- `/eleith:codex context toggle` switches the saved context preference.
|
||||
- `/eleith:codex fast on` requests priority processing after confirmation of higher allowance use.
|
||||
- `/eleith:codex fast on` checks catalog eligibility and requests priority processing after confirmation of higher allowance use.
|
||||
- `/eleith:codex fast off` requests standard processing.
|
||||
- `/eleith:codex fast toggle` switches the saved fast preference.
|
||||
- `/eleith:codex usage` fetches account allowance and reset information. in the terminal, the cancel key (Escape by default) aborts the request.
|
||||
|
||||
bare `context` and `fast` show the same dashboard as the root command, without account or catalog requests.
|
||||
fast controls require a direct official legacy `openai-codex` selection with Pi OAuth and no provider override.
|
||||
enabling priority also requires catalog eligibility.
|
||||
priority is a request, not a scheduling guarantee; reported response tiers are shown separately.
|
||||
|
||||
context and fast preferences persist per model on the active branch across reloads. the catalog baseline is not guessed:
|
||||
missing baselines are reported, and models already at or above the verified window
|
||||
get a no-extension notice rather than a smaller window.
|
||||
|
||||
local context preference replay only runs while Pi is idle. after tree navigation it waits for
|
||||
normal user input, before Pi checks compaction. custom-message-triggered turns use
|
||||
the current window without replaying pending preferences; explicit context and fast
|
||||
changes also require idle.
|
||||
|
||||
### migrate
|
||||
|
||||
when migrating from `@eleith-pi/context-codex`, run its old `/eleith:context-codex restore`
|
||||
command with that extension loaded before removing it:
|
||||
|
||||
```sh
|
||||
pi remove npm:@eleith-pi/context-codex
|
||||
pi install npm:@eleith-pi/codex
|
||||
```
|
||||
|
||||
if migrating from the older `@eleith-pi/context-window` instead, first use its
|
||||
`/eleith:context restore` command, then remove that package before installing codex.
|
||||
|
||||
branch preferences are preserved. do not load the predecessor and codex together.
|
||||
|
||||
## config
|
||||
|
||||
set a command prefix in `~/.pi/agent/extensions/eleith.json`:
|
||||
|
||||
@@ -25,7 +25,6 @@ const context: ContextStatus = {
|
||||
};
|
||||
const inherited: FastStatus = {
|
||||
preference: "inherit",
|
||||
eligibility: "unknown",
|
||||
requestPolicy: "untouched",
|
||||
lastObserved: undefined,
|
||||
conflict: false,
|
||||
@@ -63,7 +62,6 @@ it("does not present requested priority as confirmed server processing", () => {
|
||||
const fast: FastStatus = {
|
||||
...inherited,
|
||||
preference: "on",
|
||||
eligibility: "supported",
|
||||
requestPolicy: "priority",
|
||||
lastObserved: "default",
|
||||
};
|
||||
@@ -73,6 +71,21 @@ it("does not present requested priority as confirmed server processing", () => {
|
||||
expect(text).toContain("Context: 40k / 272k tokens (standard)");
|
||||
});
|
||||
|
||||
it("shows a saved on choice without needing an eligibility snapshot or a new response", () => {
|
||||
const fast: FastStatus = {
|
||||
...inherited,
|
||||
preference: "on",
|
||||
requestPolicy: "priority",
|
||||
};
|
||||
const original = structuredClone(fast);
|
||||
const text = formatCodexDashboard(context, fast, "eleith:codex");
|
||||
expect(text).toContain("Fast: On (priority requested)\nNo response tier reported yet.");
|
||||
expect(text).toContain("Priority requests use more allowance.");
|
||||
expect(text).not.toContain("Paused");
|
||||
expect(text).not.toContain("Eligibility:");
|
||||
expect(fast).toEqual(original);
|
||||
});
|
||||
|
||||
it("keeps the effective context window distinct from the selected model window", () => {
|
||||
const snapshot: ContextStatus = {
|
||||
...context,
|
||||
|
||||
@@ -36,6 +36,7 @@ type Transport = "sse" | "websocket";
|
||||
|
||||
beforeEach(blockUnexpectedNetwork);
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
@@ -50,10 +51,11 @@ it.each(["sse", "websocket"] as const)(
|
||||
const network = installResponseTransports(transport);
|
||||
const { runtime, model, accessToken, credentialWrites, resolveAuth, modelTurns } =
|
||||
await createOfflineModelRuntime(transport, network.fetch);
|
||||
const { session, manager, confirm, notify, errors, closeSession } =
|
||||
const { session, manager, confirm, notify, input, beforeStart, errors, closeSession } =
|
||||
await openRealExtensionSession(runtime, model);
|
||||
try {
|
||||
const branch = structuredClone(manager.getBranch());
|
||||
expect(resolveAuth).not.toHaveBeenCalled();
|
||||
resolveAuth.mockClear();
|
||||
let dashboard: string | undefined;
|
||||
for (const command of ["/eleith:codex", "/eleith:codex fast", "/eleith:codex context"]) {
|
||||
@@ -78,6 +80,8 @@ it.each(["sse", "websocket"] as const)(
|
||||
|
||||
await session.prompt("/eleith:codex fast on");
|
||||
expect(confirm).toHaveBeenCalledOnce();
|
||||
// Catalog discovery resolves OAuth once; there is no separate auth-scope lookup.
|
||||
expect(resolveAuth).toHaveBeenCalledExactlyOnceWith("openai-codex");
|
||||
expect(network.catalog).toHaveBeenCalledOnce();
|
||||
expect(network.catalog.mock.calls[0][0]).toBe(CATALOG_URL);
|
||||
const headers = new Headers(network.catalog.mock.calls[0][1]?.headers);
|
||||
@@ -92,10 +96,24 @@ it.each(["sse", "websocket"] as const)(
|
||||
"info",
|
||||
);
|
||||
|
||||
const enabledBranch = structuredClone(manager.getBranch());
|
||||
resolveAuth.mockClear();
|
||||
// Already on is a local no-op, even if discovery has become unavailable.
|
||||
network.catalog.mockRejectedValue(new Error("Catalog must not be consulted after enabling"));
|
||||
await session.prompt("/eleith:codex fast on");
|
||||
expect(confirm).toHaveBeenCalledOnce();
|
||||
expect(network.catalog).toHaveBeenCalledOnce();
|
||||
expect(resolveAuth).not.toHaveBeenCalled();
|
||||
expect(manager.getBranch()).toEqual(enabledBranch);
|
||||
|
||||
await session.prompt("Reply with ok.");
|
||||
// Ordinary input checks the cached catalog's auth scope through Pi resolution only.
|
||||
expect(resolveAuth).toHaveBeenCalledWith("openai-codex");
|
||||
expect(input).toHaveBeenCalledExactlyOnceWith({
|
||||
text: "Reply with ok.",
|
||||
streamingBehavior: undefined,
|
||||
authCalls: 0,
|
||||
});
|
||||
expect(beforeStart).toHaveBeenCalledExactlyOnceWith({ messageType: "user", authCalls: 0 });
|
||||
expectNativeAuthOnly(resolveAuth);
|
||||
expect(network.catalog).toHaveBeenCalledOnce();
|
||||
expect(modelTurns).toHaveBeenCalledOnce();
|
||||
expect(network.payloads).toEqual([
|
||||
@@ -169,16 +187,19 @@ it("SDK replays the active fast branch alongside context controls, explicit off
|
||||
model,
|
||||
manager,
|
||||
);
|
||||
// A resumed on preference does not depend on successful catalog discovery.
|
||||
network.catalog.mockRejectedValue(new Error("Resumed preferences must not fetch the catalog"));
|
||||
let extendedWindow: number | undefined;
|
||||
const outputLimit = session.model?.maxTokens;
|
||||
const thinking = session.thinkingLevel;
|
||||
try {
|
||||
expect(network.fetch).not.toHaveBeenCalled();
|
||||
expect(resolveAuth).not.toHaveBeenCalled();
|
||||
const branch = structuredClone(manager.getBranch());
|
||||
resolveAuth.mockClear();
|
||||
await session.prompt("/eleith:codex fast");
|
||||
expect(notify).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining("Fast: Paused — model eligibility needs checking"),
|
||||
expect.stringContaining("Fast: On (priority requested)"),
|
||||
"info",
|
||||
);
|
||||
expect(notify).toHaveBeenLastCalledWith(expect.stringContaining("Context:"), "info");
|
||||
@@ -195,9 +216,11 @@ it("SDK replays the active fast branch alongside context controls, explicit off
|
||||
expect(session.thinkingLevel).toBe(thinking);
|
||||
expect(network.fetch).not.toHaveBeenCalled();
|
||||
|
||||
// No explicit fast command fetched metadata in this resumed session.
|
||||
// Saved on immediately requests priority, without an explicit fast command or discovery.
|
||||
resolveAuth.mockClear();
|
||||
await session.prompt("Reply with ok on the resumed branch.");
|
||||
expect(network.catalog).toHaveBeenCalledOnce();
|
||||
expectNativeAuthOnly(resolveAuth);
|
||||
expect(network.catalog).not.toHaveBeenCalled();
|
||||
expect(network.payloads[0]).toMatchObject({ model: model.id, service_tier: "priority" });
|
||||
expect(fastChoices(manager)).toEqual([saved]);
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
@@ -247,7 +270,7 @@ it("SDK replays the active fast branch alongside context controls, explicit off
|
||||
expect(network.payloads).toHaveLength(3);
|
||||
expect(legacy.session.getLastAssistantText()).toBe("ok");
|
||||
expect(modelTurns).toHaveBeenCalledTimes(3);
|
||||
expect(network.catalog).toHaveBeenCalledOnce();
|
||||
expect(network.catalog).not.toHaveBeenCalled();
|
||||
expect(legacy.session.model?.contextWindow).toBe(extendedWindow);
|
||||
expect(legacy.session.model?.maxTokens).toBe(outputLimit);
|
||||
expect(legacy.session.thinkingLevel).toBe(thinking);
|
||||
@@ -259,6 +282,250 @@ it("SDK replays the active fast branch alongside context controls, explicit off
|
||||
}
|
||||
});
|
||||
|
||||
it("SDK discovers eligibility only after confirmation for each explicit transition to on", async () => {
|
||||
const network = installResponseTransports("sse");
|
||||
const { runtime, model, resolveAuth, modelTurns, credentialWrites } =
|
||||
await createOfflineModelRuntime("sse", network.fetch);
|
||||
const f = await openRealExtensionSession(runtime, model);
|
||||
const confirming = deferred();
|
||||
const decision = deferred();
|
||||
let enabling: Promise<void> | undefined;
|
||||
try {
|
||||
f.confirm.mockResolvedValueOnce(false);
|
||||
await f.session.prompt("/eleith:codex fast on");
|
||||
expect(resolveAuth).not.toHaveBeenCalled();
|
||||
expect(network.fetch).not.toHaveBeenCalled();
|
||||
expect(fastChoices(f.manager)).toEqual([]);
|
||||
|
||||
f.confirm.mockImplementationOnce(async () => {
|
||||
confirming.resolve();
|
||||
await decision.promise;
|
||||
return true;
|
||||
});
|
||||
enabling = f.session.prompt("/eleith:codex fast on");
|
||||
await confirming.promise;
|
||||
expect(resolveAuth).not.toHaveBeenCalled();
|
||||
expect(network.fetch).not.toHaveBeenCalled();
|
||||
expect(fastChoices(f.manager)).toEqual([]);
|
||||
decision.resolve();
|
||||
await enabling;
|
||||
expect(resolveAuth).toHaveBeenCalledExactlyOnceWith("openai-codex");
|
||||
expect(network.catalog).toHaveBeenCalledOnce();
|
||||
expect(fastChoices(f.manager)).toEqual([
|
||||
{ provider: "openai-codex", modelId: model.id, mode: "on" },
|
||||
]);
|
||||
|
||||
resolveAuth.mockClear();
|
||||
await f.session.prompt("/eleith:codex fast off");
|
||||
expect(resolveAuth).not.toHaveBeenCalled();
|
||||
expect(network.catalog).toHaveBeenCalledOnce();
|
||||
await f.session.prompt("/eleith:codex fast toggle");
|
||||
expect(f.confirm).toHaveBeenCalledTimes(3);
|
||||
expect(resolveAuth).toHaveBeenCalledExactlyOnceWith("openai-codex");
|
||||
expect(network.catalog).toHaveBeenCalledTimes(2);
|
||||
expect(fastChoices(f.manager)).toEqual([
|
||||
{ provider: "openai-codex", modelId: model.id, mode: "on" },
|
||||
{ provider: "openai-codex", modelId: model.id, mode: "off" },
|
||||
{ provider: "openai-codex", modelId: model.id, mode: "on" },
|
||||
]);
|
||||
expect(modelTurns).not.toHaveBeenCalled();
|
||||
expect(credentialWrites).not.toHaveBeenCalled();
|
||||
expect(f.errors).toEqual([]);
|
||||
} finally {
|
||||
decision.resolve();
|
||||
await enabling;
|
||||
await f.closeSession();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["sse", "websocket"] as const)(
|
||||
"SDK saved priority survives expiry, reload, tree replay and model reselection on %s",
|
||||
async (transport) => {
|
||||
// Fake timeout clocks too: an expiry callback must not silently revoke the saved request.
|
||||
// Microtasks and the provider's other scheduling primitives remain real.
|
||||
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"] });
|
||||
const network = installResponseTransports(transport);
|
||||
const { runtime, model, resolveAuth, modelTurns, credentialWrites } =
|
||||
await createOfflineModelRuntime(transport, network.fetch);
|
||||
const f = await openRealExtensionSession(runtime, model);
|
||||
try {
|
||||
await f.session.prompt("/eleith:codex fast on");
|
||||
expect(resolveAuth).toHaveBeenCalledExactlyOnceWith("openai-codex");
|
||||
const onLeaf = f.manager.getLeafId()!;
|
||||
const saved = { provider: "openai-codex", modelId: model.id, mode: "on" };
|
||||
network.catalog.mockRejectedValue(new Error("Prompt/lifecycle discovery forbidden"));
|
||||
resolveAuth.mockClear();
|
||||
|
||||
async function statusIsOn() {
|
||||
resolveAuth.mockClear();
|
||||
await f.session.prompt("/eleith:codex fast");
|
||||
expect(f.notify).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining("Fast: On (priority requested)"),
|
||||
"info",
|
||||
);
|
||||
expect(resolveAuth).not.toHaveBeenCalled();
|
||||
expect(network.catalog).toHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
async function priorityTurn(custom: boolean) {
|
||||
resolveAuth.mockClear();
|
||||
f.input.mockClear();
|
||||
f.beforeStart.mockClear();
|
||||
const turns = modelTurns.mock.calls.length;
|
||||
if (custom) {
|
||||
await f.session.sendCustomMessage(
|
||||
{ customType: "fast-sdk", content: "Reply with ok.", display: false },
|
||||
{ triggerTurn: true },
|
||||
);
|
||||
expect(f.input).not.toHaveBeenCalled();
|
||||
} else {
|
||||
await f.session.prompt("Reply with ok.");
|
||||
expect(f.input).toHaveBeenCalledExactlyOnceWith({
|
||||
text: "Reply with ok.",
|
||||
streamingBehavior: undefined,
|
||||
authCalls: 0,
|
||||
});
|
||||
}
|
||||
expect(f.beforeStart).toHaveBeenCalledExactlyOnceWith({
|
||||
messageType: custom ? "custom" : "user",
|
||||
authCalls: 0,
|
||||
});
|
||||
expectNativeAuthOnly(resolveAuth);
|
||||
expect(network.catalog).toHaveBeenCalledOnce();
|
||||
expect(modelTurns).toHaveBeenCalledTimes(turns + 1);
|
||||
expect(network.payloads.at(-1)).toMatchObject({
|
||||
model: model.id,
|
||||
service_tier: "priority",
|
||||
});
|
||||
expect(f.session.getLastAssistantText()).toBe("ok");
|
||||
}
|
||||
|
||||
await vi.advanceTimersByTimeAsync(6 * 60_000);
|
||||
await statusIsOn();
|
||||
await priorityTurn(true); // No idle input preparation can rescue an expired request policy.
|
||||
|
||||
resolveAuth.mockClear();
|
||||
await f.session.reload();
|
||||
expect(resolveAuth).not.toHaveBeenCalled();
|
||||
expect(network.catalog).toHaveBeenCalledOnce();
|
||||
await statusIsOn();
|
||||
await priorityTurn(false);
|
||||
|
||||
await f.session.prompt("/eleith:codex fast off");
|
||||
expect(fastChoices(f.manager)).toEqual([saved, { ...saved, mode: "off" }]);
|
||||
resolveAuth.mockClear();
|
||||
expect(await f.session.navigateTree(onLeaf)).toMatchObject({ cancelled: false });
|
||||
expect(resolveAuth).not.toHaveBeenCalled();
|
||||
expect(fastChoices(f.manager)).toEqual([saved]);
|
||||
await statusIsOn();
|
||||
await priorityTurn(true); // Tree restoration also works before any ordinary input.
|
||||
|
||||
const other = runtime.getModel("openai-codex", "gpt-6.1-sol");
|
||||
if (!other) throw new Error("Second built-in Codex model missing");
|
||||
resolveAuth.mockClear();
|
||||
await f.session.setModel(other);
|
||||
await f.session.setModel(model);
|
||||
expect(f.modelSelect).toHaveBeenCalledTimes(2); // Exercise real invalidation, not same-ID suppression.
|
||||
expect(resolveAuth).not.toHaveBeenCalled();
|
||||
await statusIsOn();
|
||||
await priorityTurn(true);
|
||||
|
||||
// Also verify idempotence after replay/invalidation, but only AFTER testing the
|
||||
// native requests so an on command cannot accidentally repair their policy.
|
||||
resolveAuth.mockClear();
|
||||
const branch = structuredClone(f.manager.getBranch());
|
||||
await f.session.prompt("/eleith:codex fast on");
|
||||
expect(f.manager.getBranch()).toEqual(branch);
|
||||
expect(resolveAuth).not.toHaveBeenCalled();
|
||||
expect(network.catalog).toHaveBeenCalledOnce();
|
||||
expect(network.payloads).toHaveLength(4);
|
||||
expect(fastChoices(f.manager)).toEqual([saved]);
|
||||
expect(f.confirm).toHaveBeenCalledOnce();
|
||||
expect(credentialWrites).not.toHaveBeenCalled();
|
||||
expect(f.errors).toEqual([]);
|
||||
} finally {
|
||||
await f.closeSession();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["prompt", "queue"] as const)(
|
||||
"SDK %s steering and follow-up input never adds auth or catalog work while fast is on",
|
||||
async (submission) => {
|
||||
const network = installResponseTransports("sse");
|
||||
const { runtime, model, resolveAuth, modelTurns, credentialWrites } =
|
||||
await createOfflineModelRuntime("sse", network.fetch);
|
||||
const f = await openRealExtensionSession(runtime, model);
|
||||
const started = deferred();
|
||||
const blocked = deferred();
|
||||
let running: Promise<void> | undefined;
|
||||
try {
|
||||
await f.session.prompt("/eleith:codex fast on");
|
||||
network.catalog.mockRejectedValue(new Error("Queued prompts must not discover eligibility"));
|
||||
const respond = network.sse.getMockImplementation()!;
|
||||
network.sse.mockImplementationOnce(async (...args) => {
|
||||
started.resolve();
|
||||
await blocked.promise;
|
||||
return respond(...args);
|
||||
});
|
||||
resolveAuth.mockClear();
|
||||
running = f.session.prompt("Start with ok.");
|
||||
await started.promise;
|
||||
expect(f.session.isStreaming).toBe(true);
|
||||
expectNativeAuthOnly(resolveAuth);
|
||||
expect(f.input).toHaveBeenCalledExactlyOnceWith({
|
||||
text: "Start with ok.",
|
||||
streamingBehavior: undefined,
|
||||
authCalls: 0,
|
||||
});
|
||||
resolveAuth.mockClear();
|
||||
f.input.mockClear();
|
||||
|
||||
if (submission === "prompt") {
|
||||
await f.session.prompt("Steer with ok.", { streamingBehavior: "steer" });
|
||||
await f.session.prompt("Follow with ok.", { streamingBehavior: "followUp" });
|
||||
} else {
|
||||
expect(await f.session.steer("Steer with ok.")).toBe("queued");
|
||||
expect(await f.session.followUp("Follow with ok.")).toBe("queued");
|
||||
}
|
||||
expect(f.input.mock.calls).toEqual([
|
||||
[{ text: "Steer with ok.", streamingBehavior: "steer", authCalls: 0 }],
|
||||
[{ text: "Follow with ok.", streamingBehavior: "followUp", authCalls: 0 }],
|
||||
]);
|
||||
expect(resolveAuth).not.toHaveBeenCalled();
|
||||
expect(network.catalog).toHaveBeenCalledOnce();
|
||||
expect(modelTurns).toHaveBeenCalledOnce();
|
||||
|
||||
blocked.resolve();
|
||||
await running;
|
||||
await f.session.waitForIdle();
|
||||
expectNativeAuthOnly(resolveAuth);
|
||||
expect(modelTurns).toHaveBeenCalledTimes(3);
|
||||
expect(network.catalog).toHaveBeenCalledOnce();
|
||||
expect(network.payloads).toEqual(
|
||||
Array.from({ length: 3 }, () =>
|
||||
expect.objectContaining({ model: model.id, service_tier: "priority" }),
|
||||
),
|
||||
);
|
||||
expect(f.confirm).toHaveBeenCalledOnce();
|
||||
expect(credentialWrites).not.toHaveBeenCalled();
|
||||
expect(f.errors).toEqual([]);
|
||||
} finally {
|
||||
blocked.resolve();
|
||||
await running;
|
||||
await f.closeSession();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function deferred() {
|
||||
let resolve!: () => void;
|
||||
const promise = new Promise<void>((done) => {
|
||||
resolve = done;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
// Network policy: globals start closed; the supplied fetch accepts only these two fixed URLs.
|
||||
function blockUnexpectedNetwork() {
|
||||
vi.stubGlobal(
|
||||
@@ -410,6 +677,9 @@ async function openRealExtensionSession(
|
||||
retry: { enabled: false },
|
||||
cacheWarming: "off",
|
||||
});
|
||||
const input = vi.fn();
|
||||
const beforeStart = vi.fn();
|
||||
const modelSelect = vi.fn();
|
||||
const resourceLoader = new DefaultResourceLoader({
|
||||
cwd: directory,
|
||||
agentDir: directory,
|
||||
@@ -419,7 +689,26 @@ async function openRealExtensionSession(
|
||||
noPromptTemplates: true,
|
||||
noThemes: true,
|
||||
noContextFiles: true,
|
||||
extensionFactories: [codex],
|
||||
extensionFactories: [
|
||||
codex,
|
||||
(pi) => {
|
||||
// These handlers run AFTER codex but BEFORE native request authentication.
|
||||
pi.on("input", (event) => {
|
||||
input({
|
||||
text: event.text,
|
||||
streamingBehavior: event.streamingBehavior,
|
||||
authCalls: vi.mocked(runtime.getAuth).mock.calls.length,
|
||||
});
|
||||
});
|
||||
pi.on("before_agent_start", (event) => {
|
||||
beforeStart({
|
||||
messageType: event.messageType,
|
||||
authCalls: vi.mocked(runtime.getAuth).mock.calls.length,
|
||||
});
|
||||
});
|
||||
pi.on("model_select", modelSelect);
|
||||
},
|
||||
],
|
||||
});
|
||||
await resourceLoader.reload();
|
||||
expect(resourceLoader.getExtensions().errors).toEqual([]);
|
||||
@@ -447,6 +736,9 @@ async function openRealExtensionSession(
|
||||
manager,
|
||||
confirm,
|
||||
notify,
|
||||
input,
|
||||
beforeStart,
|
||||
modelSelect,
|
||||
errors,
|
||||
async closeSession() {
|
||||
await session.extensionRunner.emit({ type: "session_shutdown", reason: "quit" });
|
||||
@@ -455,6 +747,16 @@ async function openRealExtensionSession(
|
||||
};
|
||||
}
|
||||
|
||||
// Native provider resolution is essential and uses a model, not the extension's provider lookup.
|
||||
function expectNativeAuthOnly(
|
||||
resolveAuth: Awaited<ReturnType<typeof createOfflineModelRuntime>>["resolveAuth"],
|
||||
) {
|
||||
expect(resolveAuth).toHaveBeenCalled();
|
||||
for (const [selected] of resolveAuth.mock.calls) {
|
||||
expect(selected).toEqual(expect.objectContaining({ provider: "openai-codex" }));
|
||||
}
|
||||
}
|
||||
|
||||
function fastChoices(manager: SessionManager) {
|
||||
return manager
|
||||
.getBranch()
|
||||
|
||||
@@ -11,10 +11,9 @@ it("describes off as a standard request, not an observed processing tier", () =>
|
||||
expect(formatFastStatus(fast)).toBe("Fast: Off (standard requested)");
|
||||
});
|
||||
|
||||
it("describes on as a priority request without claiming priority was delivered", () => {
|
||||
it("describes a saved on choice as priority without eligibility state or a reported tier", () => {
|
||||
const fast = createFastStatusFixture({
|
||||
preference: "on",
|
||||
eligibility: "supported",
|
||||
requestPolicy: "priority",
|
||||
});
|
||||
expect(formatFastStatus(fast).split("\n")).toEqual([
|
||||
@@ -27,7 +26,6 @@ it("describes on as a priority request without claiming priority was delivered",
|
||||
it("keeps next-request priority separate from a reported standard tier", () => {
|
||||
const fast = createFastStatusFixture({
|
||||
preference: "on",
|
||||
eligibility: "supported",
|
||||
requestPolicy: "priority",
|
||||
lastObserved: "default",
|
||||
});
|
||||
@@ -51,34 +49,16 @@ it.each<[ObservedTier, string]>([
|
||||
);
|
||||
});
|
||||
|
||||
it("pauses a saved on choice when eligibility needs checking without claiming a current request", () => {
|
||||
it("keeps a saved on choice intact outside scope and shows priority again when in scope", () => {
|
||||
const fast = createFastStatusFixture({
|
||||
preference: "on",
|
||||
requestPolicy: "suspended",
|
||||
});
|
||||
expect(formatFastStatus(fast)).toBe("Fast: Paused — model eligibility needs checking");
|
||||
});
|
||||
|
||||
it("explains when priority is not available without erasing the saved preference", () => {
|
||||
const fast = createFastStatusFixture({
|
||||
preference: "on",
|
||||
eligibility: "unsupported",
|
||||
requestPolicy: "suspended",
|
||||
lastObserved: "default",
|
||||
});
|
||||
expect(formatFastStatus(fast)).toBe(
|
||||
"Fast: Paused — priority is not available for this model\nLast reported tier: Standard",
|
||||
);
|
||||
expect(fast.preference).toBe("on");
|
||||
});
|
||||
|
||||
it("gives outside scope precedence over the saved on preference and supported eligibility", () => {
|
||||
const fast = createFastStatusFixture({
|
||||
preference: "on",
|
||||
eligibility: "supported",
|
||||
requestPolicy: "outside scope",
|
||||
});
|
||||
expect(formatFastStatus(fast)).toBe("Fast: Unavailable for this model/provider");
|
||||
expect(fast.preference).toBe("on");
|
||||
expect(formatFastStatus({ ...fast, requestPolicy: "priority" })).toBe(
|
||||
"Fast: On (priority requested)\nNo response tier reported yet.\nPriority requests use more allowance.",
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["priority", "default"] as const)(
|
||||
@@ -86,7 +66,6 @@ it.each(["priority", "default"] as const)(
|
||||
(requestPolicy) => {
|
||||
const fast = createFastStatusFixture({
|
||||
preference: requestPolicy === "priority" ? "on" : "off",
|
||||
eligibility: "supported",
|
||||
requestPolicy,
|
||||
lastObserved: "flex",
|
||||
conflict: true,
|
||||
@@ -111,7 +90,6 @@ it.each(["priority", "default"] as const)(
|
||||
function createFastStatusFixture(overrides: Partial<FastStatus> = {}): FastStatus {
|
||||
return {
|
||||
preference: "inherit",
|
||||
eligibility: "unknown",
|
||||
requestPolicy: "untouched",
|
||||
lastObserved: undefined,
|
||||
conflict: false,
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import type { FastEligibility } from "./capabilities.ts";
|
||||
import type { FastPreference } from "./fast-state.ts";
|
||||
|
||||
export type RequestedTier = "priority" | "default";
|
||||
export type ObservedTier = RequestedTier | "auto" | "flex" | "fast";
|
||||
export type FastRequestPolicy = RequestedTier | "untouched" | "suspended" | "outside scope";
|
||||
export type FastRequestPolicy = RequestedTier | "untouched" | "outside scope";
|
||||
|
||||
export interface FastStatus {
|
||||
preference: FastPreference;
|
||||
eligibility: FastEligibility;
|
||||
requestPolicy: FastRequestPolicy;
|
||||
lastObserved: ObservedTier | undefined;
|
||||
conflict: boolean;
|
||||
@@ -25,12 +23,6 @@ export function formatFastStatus(status: FastStatus): string {
|
||||
case "priority":
|
||||
headline = "Fast: On (priority requested)";
|
||||
break;
|
||||
case "suspended":
|
||||
headline =
|
||||
status.eligibility === "unsupported"
|
||||
? "Fast: Paused — priority is not available for this model"
|
||||
: "Fast: Paused — model eligibility needs checking";
|
||||
break;
|
||||
case "outside scope":
|
||||
headline = "Fast: Unavailable for this model/provider";
|
||||
break;
|
||||
|
||||
+184
-346
@@ -9,7 +9,6 @@ import {
|
||||
CodexAccountError,
|
||||
type CodexModelCatalog,
|
||||
type fetchCodexModelCatalog,
|
||||
type readCodexAuthScope,
|
||||
} from "./client.ts";
|
||||
import { FastController, type FastHost } from "./fast.ts";
|
||||
import { FAST_ENTRY, type FastPreference } from "./fast-state.ts";
|
||||
@@ -47,11 +46,9 @@ describe("commands and ownership", () => {
|
||||
);
|
||||
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
|
||||
preference: "on",
|
||||
eligibility: "unknown",
|
||||
requestPolicy: "suspended",
|
||||
requestPolicy: "priority",
|
||||
});
|
||||
expect(fast.ctx.ui.confirm).not.toHaveBeenCalled();
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
expect(fast.ctx.modelRegistry.getProviderAuth).not.toHaveBeenCalled();
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
@@ -67,7 +64,6 @@ describe("commands and ownership", () => {
|
||||
expect.stringContaining("use more of your Codex allowance"),
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
@@ -88,17 +84,11 @@ describe("commands and ownership", () => {
|
||||
model: model.id,
|
||||
service_tier: "default",
|
||||
});
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects invalid arguments and mutations during a run without prompting or IO", async () => {
|
||||
it("rejects mutations during a run without prompting or IO", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
await fast.controller.handle("on extra", fast.ctx);
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
"Usage: /eleith:codex fast [on|off|toggle]",
|
||||
"warning",
|
||||
);
|
||||
fast.ctx.isIdle.mockReturnValue(false);
|
||||
for (const action of ["on", "off", "toggle"]) await fast.controller.handle(action, fast.ctx);
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
@@ -106,7 +96,6 @@ describe("commands and ownership", () => {
|
||||
"warning",
|
||||
);
|
||||
expect(fast.ctx.ui.confirm).not.toHaveBeenCalled();
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -117,7 +106,6 @@ describe("commands and ownership", () => {
|
||||
await fast.controller.handle("on", fast.ctx);
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith("No model selected.", "warning");
|
||||
expect(fast.ctx.ui.confirm).not.toHaveBeenCalled();
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -133,10 +121,8 @@ describe("commands and ownership", () => {
|
||||
return response.promise;
|
||||
});
|
||||
const enabling = fast.controller.handle(" ON ", fast.ctx);
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
// A second mutating command must not compete with the confirmation.
|
||||
await fast.controller.handle("off", fast.ctx);
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining("Wait for the current run"),
|
||||
@@ -144,14 +130,12 @@ describe("commands and ownership", () => {
|
||||
);
|
||||
confirmation.resolve(true);
|
||||
await started.promise;
|
||||
expect(fast.readAuthScope).toHaveBeenCalledExactlyOnceWith(
|
||||
fast.ctx.modelRegistry,
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledExactlyOnceWith(
|
||||
fast.ctx.modelRegistry,
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
// No separate auth-scope preflight; the account client resolves auth for its one request.
|
||||
expect(fast.ctx.modelRegistry.getProviderAuth).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
response.resolve(catalog);
|
||||
@@ -172,24 +156,43 @@ describe("commands and ownership", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ models: [{ slug: otherModel.id, service_tiers: [{ id: "priority" }] }] },
|
||||
{ models: [{ slug: model.id, service_tiers: [{ id: "default" }] }] },
|
||||
])(
|
||||
"does not enable without priority support for the selected catalog model: %j",
|
||||
async (data) => {
|
||||
const fast = createFastControllerFixture();
|
||||
fast.fetchCatalog.mockResolvedValue({ ...catalog, data });
|
||||
await fast.controller.handle("on", fast.ctx);
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining("Preference unchanged"),
|
||||
"warning",
|
||||
);
|
||||
{
|
||||
data: { models: [{ slug: otherModel.id, service_tiers: [{ id: "priority" }] }] },
|
||||
reason: "did not establish",
|
||||
},
|
||||
);
|
||||
{
|
||||
data: { models: [{ slug: model.id, service_tiers: [{ id: "default" }] }] },
|
||||
reason: "does not advertise",
|
||||
},
|
||||
])("does not enable without established model support: %j", async ({ data, reason }) => {
|
||||
const fast = createFastControllerFixture();
|
||||
fast.fetchCatalog.mockResolvedValue({ ...catalog, data });
|
||||
await fast.controller.handle("on", fast.ctx);
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining(reason), "warning");
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining("Preference unchanged"),
|
||||
"warning",
|
||||
);
|
||||
});
|
||||
|
||||
it("copies a frozen payload for explicit default, preserves fields, then restores a legacy inherit entry", async () => {
|
||||
it("treats an already saved on as idempotent without confirmation or discovery", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
fast.ctx.sessionManager.getBranch.mockReturnValue([choice("on")]);
|
||||
fast.controller.restore(fast.ctx);
|
||||
fast.ctx.hasUI = false;
|
||||
await fast.controller.handle("on", fast.ctx);
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining("Fast: On (priority requested)"),
|
||||
"info",
|
||||
);
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
expect(fast.ctx.ui.confirm).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("copies a frozen payload for explicit default and restores a legacy inherit entry", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
const input = Object.freeze([{ role: "user", content: "hello" }]);
|
||||
const payload = Object.freeze({
|
||||
@@ -222,29 +225,8 @@ describe("commands and ownership", () => {
|
||||
preference: "inherit",
|
||||
requestPolicy: "untouched",
|
||||
});
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
fast.controller.restore(fast.ctx);
|
||||
expect(fast.appendEntry).toHaveBeenCalledOnce();
|
||||
expect(fast.ctx.ui.confirm).not.toHaveBeenCalled();
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("restores a legacy inherit entry after the selection leaves direct scope without writing", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
await fast.controller.handle("off", fast.ctx);
|
||||
fast.ctx.modelRegistry.isUsingOAuth.mockReturnValue(false);
|
||||
fast.ctx.sessionManager.getBranch.mockReturnValue([choice("off"), choice("inherit")]);
|
||||
fast.controller.restore(fast.ctx);
|
||||
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
|
||||
preference: "inherit",
|
||||
requestPolicy: "outside scope",
|
||||
});
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
expect(fast.appendEntry).toHaveBeenCalledOnce();
|
||||
expect(fast.ctx.ui.confirm).not.toHaveBeenCalled();
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(["on", "off"])(
|
||||
@@ -264,17 +246,13 @@ describe("commands and ownership", () => {
|
||||
model: model.id,
|
||||
service_tier: tier,
|
||||
});
|
||||
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
|
||||
conflict: false,
|
||||
requestPolicy: tier,
|
||||
});
|
||||
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({ conflict: false });
|
||||
},
|
||||
);
|
||||
|
||||
it("toggle confirms only transitions to on and leaves off intact if the next confirmation is declined", async () => {
|
||||
it("toggle confirms only transitions to on and preserves off if confirmation is declined", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
await fast.controller.handle("toggle", fast.ctx);
|
||||
expect(fast.ctx.ui.confirm).toHaveBeenCalledOnce();
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
|
||||
await fast.controller.handle("toggle", fast.ctx);
|
||||
expect(fast.ctx.ui.confirm).toHaveBeenCalledOnce();
|
||||
@@ -289,25 +267,56 @@ describe("commands and ownership", () => {
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("default");
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("honors server Retry-After only for explicit enable attempts, without automatic retries", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
fast.fetchCatalog.mockRejectedValueOnce(
|
||||
new CodexAccountError("http", "Codex account service is rate limited.", 60_000),
|
||||
);
|
||||
await fast.controller.handle("on", fast.ctx);
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
"Codex account service is rate limited. Preference unchanged.",
|
||||
"warning",
|
||||
);
|
||||
await fast.controller.handle("on", fast.ctx);
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledOnce();
|
||||
expect(fast.ctx.ui.confirm).toHaveBeenCalledOnce();
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining("Try fast on again later"),
|
||||
"warning",
|
||||
);
|
||||
fast.clock.now += 60_000;
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledOnce();
|
||||
await fast.controller.handle("on", fast.ctx);
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledTimes(2);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
|
||||
});
|
||||
|
||||
it("does not expose unexpected discovery errors or change an existing off preference", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
await fast.controller.handle("off", fast.ctx);
|
||||
fast.fetchCatalog.mockRejectedValueOnce(new Error("private failure details must not appear"));
|
||||
await fast.controller.handle("on", fast.ctx);
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
"Could not enable Codex fast mode. Preference unchanged.",
|
||||
"warning",
|
||||
);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("default");
|
||||
expect(fast.appendEntry).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe("branch replay and preparation", () => {
|
||||
it("replays only the current branch, separates model choices, and ignores context choices", async () => {
|
||||
describe("durable branch preferences", () => {
|
||||
it("replays the active branch immediately without discovery, keeping model and context choices separate", () => {
|
||||
const fast = createFastControllerFixture();
|
||||
fast.ctx.sessionManager.getBranch.mockReturnValue([
|
||||
choice("off"),
|
||||
choice("on"),
|
||||
choice("off", otherModel),
|
||||
{
|
||||
...choice("off"),
|
||||
customType: CONTEXT_ENTRY,
|
||||
data: { provider: model.provider, modelId: model.id, extended: true, mode: "off" },
|
||||
},
|
||||
{ ...choice("off"), customType: CONTEXT_ENTRY },
|
||||
]);
|
||||
fast.controller.restore(fast.ctx);
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
|
||||
fast.ctx.model = otherModel;
|
||||
expect(fast.controller.transform({ model: otherModel.id }, fast.ctx)?.service_tier).toBe(
|
||||
@@ -318,171 +327,60 @@ describe("branch replay and preparation", () => {
|
||||
expect(fast.controller.transform({ model: otherModel.id }, fast.ctx)).toBeUndefined();
|
||||
fast.ctx.model = model;
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
expect(fast.readAuthScope).toHaveBeenCalledOnce();
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledOnce();
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
expect(fast.ctx.modelRegistry.getProviderAuth).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
expect(fast.ctx.ui.confirm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("lets fresh on expire at the TTL without hook IO or erasing the suspended preference", async () => {
|
||||
it("keeps priority after five minutes, days, invalidation and reload without any preflight", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
await fast.controller.handle("on", fast.ctx);
|
||||
fast.clock.now += 5 * 60_000 - 1;
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
|
||||
fast.clock.now += 1;
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
fast.controller.observe(
|
||||
stream({ type: "response.completed", response: { service_tier: "default" } }),
|
||||
fast.ctx,
|
||||
);
|
||||
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
|
||||
preference: "on",
|
||||
eligibility: "unknown",
|
||||
requestPolicy: "suspended",
|
||||
});
|
||||
expect(fast.readAuthScope).toHaveBeenCalledOnce();
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledOnce();
|
||||
expect(fast.appendEntry).toHaveBeenCalledOnce();
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledTimes(2);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
|
||||
expect(fast.appendEntry).toHaveBeenCalledOnce();
|
||||
expect(fast.ctx.ui.confirm).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("prepares only idle saved-on input, checking auth even when the catalog cache is fresh", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
for (const branch of [[], [choice("off")], [choice("inherit")]]) {
|
||||
fast.ctx.sessionManager.getBranch.mockReturnValue(branch);
|
||||
fast.controller.restore(fast.ctx);
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
}
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
fast.ctx.sessionManager.getBranch.mockReturnValue([choice("on")]);
|
||||
fast.controller.restore(fast.ctx);
|
||||
fast.fetchCatalog.mockRejectedValue(new Error("Catalog must not be needed again"));
|
||||
fast.ctx.modelRegistry.getProviderAuth.mockRejectedValue(new Error("No auth preflight"));
|
||||
fast.ctx.isIdle.mockReturnValue(false);
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
fast.ctx.isIdle.mockReturnValue(true);
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
expect(fast.readAuthScope).toHaveBeenCalledTimes(2);
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledOnce();
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
|
||||
expect(fast.ctx.ui.confirm).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("invalidates a fresh catalog on auth scope change and recatalogs before resuming", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
fast.ctx.sessionManager.getBranch.mockReturnValue([choice("on")]);
|
||||
fast.controller.restore(fast.ctx);
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
fast.readAuthScope.mockResolvedValue("scope-b");
|
||||
fast.fetchCatalog.mockResolvedValue({
|
||||
authScope: "scope-b",
|
||||
data: { models: [{ slug: model.id, service_tiers: [{ id: "default" }] }] },
|
||||
});
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledTimes(2);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
|
||||
preference: "on",
|
||||
eligibility: "unsupported",
|
||||
requestPolicy: "suspended",
|
||||
});
|
||||
fast.readAuthScope.mockResolvedValue("scope-c");
|
||||
fast.fetchCatalog.mockResolvedValue({ ...catalog, authScope: "scope-c" });
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledTimes(3);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects a catalog fetched under a different auth scope, preserving the preference for a retry", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
fast.ctx.sessionManager.getBranch.mockReturnValue([choice("on")]);
|
||||
fast.controller.restore(fast.ctx);
|
||||
fast.fetchCatalog.mockResolvedValueOnce({ ...catalog, authScope: "scope-b" });
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining("authentication changed during discovery"),
|
||||
"warning",
|
||||
);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledTimes(2);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("honors 429 backoff without timers or hook retries and resumes only on later input", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
fast.ctx.sessionManager.getBranch.mockReturnValue([choice("on")]);
|
||||
fast.controller.restore(fast.ctx);
|
||||
fast.fetchCatalog.mockRejectedValueOnce(
|
||||
new CodexAccountError("http", "Codex account service is rate limited.", 60_000),
|
||||
);
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining("rate limited. Preference unchanged."),
|
||||
"warning",
|
||||
);
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledOnce();
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining("Try again later"),
|
||||
"warning",
|
||||
);
|
||||
fast.clock.now += 60_000;
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
fast.controller.observe(
|
||||
stream({ type: "response.completed", response: { service_tier: "priority" } }),
|
||||
fast.ctx,
|
||||
);
|
||||
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
|
||||
preference: "on",
|
||||
requestPolicy: "suspended",
|
||||
lastObserved: "priority",
|
||||
});
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledOnce();
|
||||
expect(fast.readAuthScope).toHaveBeenCalledTimes(2);
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledTimes(2);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(["auth", "catalog"])(
|
||||
"does not display unexpected %s errors or clear a saved-on preference",
|
||||
async (source) => {
|
||||
const fast = createFastControllerFixture();
|
||||
fast.ctx.sessionManager.getBranch.mockReturnValue([choice("on")]);
|
||||
fast.controller.restore(fast.ctx);
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
const error = new Error("private failure details must not appear");
|
||||
if (source === "auth") fast.readAuthScope.mockRejectedValueOnce(error);
|
||||
else {
|
||||
fast.clock.now += 5 * 60_000;
|
||||
fast.fetchCatalog.mockRejectedValueOnce(error);
|
||||
}
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
"Fast mode suspended: Could not prepare Codex fast mode. Preference unchanged.",
|
||||
"warning",
|
||||
for (const elapsed of [5 * 60_000, 24 * 60 * 60_000]) {
|
||||
fast.clock.now += elapsed;
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe(
|
||||
"priority",
|
||||
);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
|
||||
preference: "on",
|
||||
requestPolicy: "suspended",
|
||||
});
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({ requestPolicy: "priority" });
|
||||
}
|
||||
fast.controller.invalidate();
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
|
||||
fast.ctx.sessionManager.getBranch.mockReturnValue([choice("on")]);
|
||||
const reloaded = new FastController(
|
||||
{ appendEntry: fast.appendEntry },
|
||||
"eleith:codex",
|
||||
fast.fetchCatalog,
|
||||
);
|
||||
reloaded.restore(fast.ctx);
|
||||
expect(reloaded.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledOnce();
|
||||
expect(fast.ctx.modelRegistry.getProviderAuth).not.toHaveBeenCalled();
|
||||
expect(fast.ctx.ui.confirm).toHaveBeenCalledOnce();
|
||||
expect(fast.ctx.ui.notify).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("keeps saved on while outside scope and resumes it when the direct selection returns", () => {
|
||||
const fast = createFastControllerFixture();
|
||||
fast.ctx.sessionManager.getBranch.mockReturnValue([choice("on")]);
|
||||
fast.controller.restore(fast.ctx);
|
||||
fast.ctx.modelRegistry.isUsingOAuth.mockReturnValue(false);
|
||||
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
|
||||
preference: "on",
|
||||
requestPolicy: "outside scope",
|
||||
});
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
fast.ctx.modelRegistry.isUsingOAuth.mockReturnValue(true);
|
||||
fast.controller.invalidate();
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("asynchronous ownership", () => {
|
||||
describe("asynchronous enable ownership", () => {
|
||||
it.each(["model", "session", "leaf"])(
|
||||
"discards confirmation after the %s changes",
|
||||
async (changed) => {
|
||||
@@ -490,12 +388,9 @@ describe("asynchronous ownership", () => {
|
||||
const confirmation = deferred<boolean>();
|
||||
fast.ctx.ui.confirm.mockReturnValue(confirmation.promise);
|
||||
const enabling = fast.controller.handle("on", fast.ctx);
|
||||
if (changed === "model") fast.ctx.model = otherModel;
|
||||
if (changed === "session") fast.ctx.sessionManager.getSessionId.mockReturnValue("session-b");
|
||||
if (changed === "leaf") fast.ctx.sessionManager.getLeafId.mockReturnValue("leaf-b");
|
||||
changeSelection(fast, changed);
|
||||
confirmation.resolve(true);
|
||||
await enabling;
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
expect(fast.ctx.ui.notify).not.toHaveBeenCalled();
|
||||
@@ -514,9 +409,7 @@ describe("asynchronous ownership", () => {
|
||||
});
|
||||
const enabling = fast.controller.handle("on", fast.ctx);
|
||||
await started.promise;
|
||||
if (changed === "model") fast.ctx.model = otherModel;
|
||||
if (changed === "session") fast.ctx.sessionManager.getSessionId.mockReturnValue("session-b");
|
||||
if (changed === "leaf") fast.ctx.sessionManager.getLeafId.mockReturnValue("leaf-b");
|
||||
changeSelection(fast, changed);
|
||||
response.resolve(catalog);
|
||||
await enabling;
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
@@ -559,113 +452,62 @@ describe("asynchronous ownership", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["resolve", "reject"])(
|
||||
"shutdown ignores late confirmation %s before touching expired host getters",
|
||||
async (outcome) => {
|
||||
const fast = createFastControllerFixture();
|
||||
const confirmation = deferred<boolean>();
|
||||
fast.ctx.ui.confirm.mockReturnValue(confirmation.promise);
|
||||
const enabling = fast.controller.handle("on", fast.ctx);
|
||||
const signal = fast.ctx.ui.confirm.mock.calls[0][2]!.signal!;
|
||||
const notify = fast.ctx.ui.notify;
|
||||
fast.controller.dispose();
|
||||
fast.controller.dispose();
|
||||
for (const key of ["model", "modelRegistry", "isIdle", "sessionManager", "ui", "hasUI"]) {
|
||||
Object.defineProperty(fast.ctx, key, {
|
||||
get: () => {
|
||||
throw new Error(`Expired ${key}`);
|
||||
},
|
||||
it.each(["confirmation", "catalog"])(
|
||||
"shutdown ignores late %s results before touching expired host getters",
|
||||
async (stage) => {
|
||||
for (const outcome of ["resolve", "reject"]) {
|
||||
const fast = createFastControllerFixture();
|
||||
const confirmation = deferred<boolean>();
|
||||
const response = deferred<CodexModelCatalog>();
|
||||
const started = deferred<void>();
|
||||
if (stage === "confirmation") fast.ctx.ui.confirm.mockReturnValue(confirmation.promise);
|
||||
fast.fetchCatalog.mockImplementationOnce(() => {
|
||||
started.resolve();
|
||||
return response.promise;
|
||||
});
|
||||
const enabling = fast.controller.handle("on", fast.ctx);
|
||||
if (stage === "catalog") await started.promise;
|
||||
const signal =
|
||||
stage === "confirmation"
|
||||
? fast.ctx.ui.confirm.mock.calls[0][2]!.signal!
|
||||
: fast.fetchCatalog.mock.calls[0][1]!;
|
||||
const notify = fast.ctx.ui.notify;
|
||||
fast.controller.dispose();
|
||||
fast.controller.dispose();
|
||||
for (const key of ["model", "modelRegistry", "isIdle", "sessionManager", "ui", "hasUI"]) {
|
||||
Object.defineProperty(fast.ctx, key, {
|
||||
get: () => {
|
||||
throw new Error(`Expired ${key}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
if (stage === "confirmation") {
|
||||
if (outcome === "resolve") confirmation.resolve(true);
|
||||
else confirmation.reject(new Error("private late failure"));
|
||||
} else {
|
||||
if (outcome === "resolve") response.resolve(catalog);
|
||||
else response.reject(new CodexAccountError("http", "private late failure", 60_000));
|
||||
}
|
||||
await enabling;
|
||||
expect(signal.aborted).toBe(true);
|
||||
expect(notify).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
expect(fast.controller.getStatus(fast.ctx)).toBeUndefined();
|
||||
await fast.controller.handle("on", fast.ctx);
|
||||
fast.controller.restore(fast.ctx);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
fast.controller.observe(stream({ type: "response.completed" }), fast.ctx);
|
||||
}
|
||||
if (outcome === "resolve") confirmation.resolve(true);
|
||||
else confirmation.reject(new Error("private late failure"));
|
||||
await enabling;
|
||||
expect(signal.aborted).toBe(true);
|
||||
expect(notify).not.toHaveBeenCalled();
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
expect(fast.controller.getStatus(fast.ctx)).toBeUndefined();
|
||||
await fast.controller.handle("on", fast.ctx);
|
||||
await fast.controller.prepare(fast.ctx);
|
||||
fast.controller.restore(fast.ctx);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
fast.controller.observe(stream({ type: "response.completed" }), fast.ctx);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["resolve", "reject"])(
|
||||
"shutdown ignores late catalog %s without accessing the expired host",
|
||||
async (outcome) => {
|
||||
const fast = createFastControllerFixture();
|
||||
const response = deferred<CodexModelCatalog>();
|
||||
const started = deferred<void>();
|
||||
fast.fetchCatalog.mockImplementationOnce(() => {
|
||||
started.resolve();
|
||||
return response.promise;
|
||||
});
|
||||
const enabling = fast.controller.handle("on", fast.ctx);
|
||||
await started.promise;
|
||||
const signal = fast.fetchCatalog.mock.calls[0][1]!;
|
||||
const notify = fast.ctx.ui.notify;
|
||||
fast.controller.dispose();
|
||||
for (const key of ["model", "modelRegistry", "isIdle", "sessionManager", "ui"]) {
|
||||
Object.defineProperty(fast.ctx, key, {
|
||||
get: () => {
|
||||
throw new Error(`Expired ${key}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
if (outcome === "resolve") response.resolve(catalog);
|
||||
else
|
||||
response.reject(
|
||||
new CodexAccountError("http", "Codex account service is rate limited.", 60_000),
|
||||
);
|
||||
await enabling;
|
||||
expect(signal.aborted).toBe(true);
|
||||
expect(notify).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("shutdown stops a saved-on auth check before any late catalog IO or host access", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
fast.ctx.sessionManager.getBranch.mockReturnValue([choice("on")]);
|
||||
fast.controller.restore(fast.ctx);
|
||||
const scope = deferred<string>();
|
||||
fast.readAuthScope.mockReturnValueOnce(scope.promise);
|
||||
const preparing = fast.controller.prepare(fast.ctx);
|
||||
const signal = fast.readAuthScope.mock.calls[0][1]!;
|
||||
const notify = fast.ctx.ui.notify;
|
||||
fast.controller.dispose();
|
||||
for (const key of ["model", "modelRegistry", "isIdle", "sessionManager", "ui"]) {
|
||||
Object.defineProperty(fast.ctx, key, {
|
||||
get: () => {
|
||||
throw new Error(`Expired ${key}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
scope.resolve("scope-a");
|
||||
await preparing;
|
||||
expect(signal.aborted).toBe(true);
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
expect(notify).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("provider stream observations", () => {
|
||||
it("reports an observed default independently of the priority payload and next-request policy", async () => {
|
||||
it("reports observed default independently of the priority request without disabling future requests", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
await fast.controller.handle("on", fast.ctx);
|
||||
const payload = Object.freeze({ model: model.id, input: [], service_tier: "priority" });
|
||||
const transformed = fast.controller.transform(payload, fast.ctx);
|
||||
expect(transformed).toBeUndefined();
|
||||
expect(transformed ?? payload).toEqual({
|
||||
model: model.id,
|
||||
input: [],
|
||||
service_tier: "priority",
|
||||
});
|
||||
expect(fast.controller.transform(payload, fast.ctx)).toBeUndefined();
|
||||
fast.controller.observe(
|
||||
stream({ type: "response.completed", response: { service_tier: "default" } }),
|
||||
fast.ctx,
|
||||
@@ -677,7 +519,6 @@ describe("provider stream observations", () => {
|
||||
});
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
|
||||
expect(fast.fetchCatalog).toHaveBeenCalledOnce();
|
||||
expect(fast.readAuthScope).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -686,10 +527,7 @@ describe("provider stream observations", () => {
|
||||
])("reports missing or unrecognized terminal tier metadata as unknown: %j", async (data) => {
|
||||
const fast = createFastControllerFixture();
|
||||
await fast.controller.handle("off", fast.ctx);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toEqual({
|
||||
model: model.id,
|
||||
service_tier: "default",
|
||||
});
|
||||
fast.controller.transform({ model: model.id }, fast.ctx);
|
||||
fast.controller.observe(
|
||||
stream({ type: "response.completed", response: { service_tier: "default" } }),
|
||||
fast.ctx,
|
||||
@@ -699,17 +537,13 @@ describe("provider stream observations", () => {
|
||||
requestPolicy: "default",
|
||||
lastObserved: undefined,
|
||||
});
|
||||
expect(fast.readAuthScope).not.toHaveBeenCalled();
|
||||
expect(fast.fetchCatalog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores wrong-model and nonterminal events, and does not show delivery from another session", async () => {
|
||||
it("ignores wrong-model and nonterminal events, and delivery from another session", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
await fast.controller.handle("off", fast.ctx);
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toEqual({
|
||||
model: model.id,
|
||||
service_tier: "default",
|
||||
});
|
||||
fast.controller.transform({ model: model.id }, fast.ctx);
|
||||
fast.controller.observe(
|
||||
stream({ type: "response.completed", response: { service_tier: "default" } }),
|
||||
fast.ctx,
|
||||
@@ -795,15 +629,19 @@ function createFastControllerFixture() {
|
||||
} satisfies FastHost;
|
||||
const appendEntry = vi.fn<ExtensionAPI["appendEntry"]>();
|
||||
const fetchCatalog = vi.fn<typeof fetchCodexModelCatalog>(async () => catalog);
|
||||
const readAuthScope = vi.fn<typeof readCodexAuthScope>(async () => "scope-a");
|
||||
const controller = new FastController(
|
||||
{ appendEntry },
|
||||
"eleith:codex",
|
||||
fetchCatalog,
|
||||
readAuthScope,
|
||||
() => clock.now,
|
||||
);
|
||||
return { controller, ctx, appendEntry, fetchCatalog, readAuthScope, clock };
|
||||
return { controller, ctx, appendEntry, fetchCatalog, clock };
|
||||
}
|
||||
|
||||
function changeSelection(fast: ReturnType<typeof createFastControllerFixture>, changed: string) {
|
||||
if (changed === "model") fast.ctx.model = otherModel;
|
||||
if (changed === "session") fast.ctx.sessionManager.getSessionId.mockReturnValue("session-b");
|
||||
if (changed === "leaf") fast.ctx.sessionManager.getLeafId.mockReturnValue("leaf-b");
|
||||
}
|
||||
|
||||
function stream(data: unknown): ProviderStreamEvent {
|
||||
|
||||
+28
-102
@@ -3,13 +3,8 @@ import type {
|
||||
ExtensionContext,
|
||||
ProviderStreamEvent,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import {
|
||||
CodexAccountError,
|
||||
fetchCodexModelCatalog,
|
||||
readCodexAuthScope,
|
||||
type CodexAccountRegistry,
|
||||
} from "./client.ts";
|
||||
import { codexFastEligibility, type FastEligibility } from "./capabilities.ts";
|
||||
import { CodexAccountError, fetchCodexModelCatalog, type CodexAccountRegistry } from "./client.ts";
|
||||
import { codexFastEligibility } from "./capabilities.ts";
|
||||
import {
|
||||
FAST_ENTRY,
|
||||
readFastPreferences,
|
||||
@@ -29,8 +24,6 @@ import {
|
||||
type DirectCodexHost,
|
||||
} from "./request-scope.ts";
|
||||
|
||||
const CAPABILITY_TTL_MS = 5 * 60_000;
|
||||
|
||||
export interface FastHost extends DirectCodexHost {
|
||||
readonly modelRegistry: CodexAccountRegistry &
|
||||
Pick<
|
||||
@@ -55,13 +48,6 @@ interface EligibilityPreparation {
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
interface CachedEligibility {
|
||||
modelKey: string;
|
||||
authScope: string;
|
||||
eligibility: FastEligibility;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
interface ModelTierActivity {
|
||||
modelKey: string;
|
||||
sessionId: string;
|
||||
@@ -71,7 +57,6 @@ interface ModelTierActivity {
|
||||
|
||||
export class FastController {
|
||||
private preferences = new Map<string, FastPreference>();
|
||||
private cachedEligibility: CachedEligibility | undefined;
|
||||
private pendingPreparation: EligibilityPreparation | undefined;
|
||||
private modelTierActivity: ModelTierActivity | undefined;
|
||||
// Honor endpoint Retry-After across model/branch changes; account switching is not supported.
|
||||
@@ -82,7 +67,6 @@ export class FastController {
|
||||
private readonly pi: Pick<ExtensionAPI, "appendEntry">,
|
||||
private readonly commandName: string,
|
||||
private readonly fetchCatalog: typeof fetchCodexModelCatalog = fetchCodexModelCatalog,
|
||||
private readonly readAuthScope: typeof readCodexAuthScope = readCodexAuthScope,
|
||||
private readonly now: () => number = Date.now,
|
||||
) {}
|
||||
|
||||
@@ -106,11 +90,8 @@ export class FastController {
|
||||
activity.sessionId !== ctx.sessionManager.getSessionId()
|
||||
)
|
||||
activity = undefined;
|
||||
let eligibility: FastEligibility = "unknown";
|
||||
if (direct) eligibility = this.currentEligibility(selectedModelKey);
|
||||
return {
|
||||
preference: this.preferenceFor(selectedModelKey),
|
||||
eligibility,
|
||||
requestPolicy: this.requestPolicy(ctx),
|
||||
lastObserved: activity?.observed,
|
||||
conflict: activity?.conflict ?? false,
|
||||
@@ -150,6 +131,10 @@ export class FastController {
|
||||
this.report(ctx);
|
||||
return;
|
||||
}
|
||||
if (this.preferenceFor(modelKey(model)) === "on") {
|
||||
this.report(ctx);
|
||||
return;
|
||||
}
|
||||
if (!ctx.hasUI) {
|
||||
ctx.ui.notify(
|
||||
"Enabling fast mode requires interactive confirmation of higher allowance consumption.",
|
||||
@@ -160,22 +145,6 @@ export class FastController {
|
||||
await this.enable(ctx);
|
||||
}
|
||||
|
||||
// Only ordinary idle input prepares saved on choices. Never fetch from request/stream hooks.
|
||||
async prepare(ctx: FastHost): Promise<void> {
|
||||
if (this.disposed || this.pendingPreparation || !ctx.isIdle()) return;
|
||||
const model = ctx.model;
|
||||
if (!model || this.preferenceFor(modelKey(model)) !== "on" || !isDirectCodexSelection(ctx))
|
||||
return;
|
||||
const preparation = this.startPreparation(ctx);
|
||||
try {
|
||||
await this.refreshEligibility(ctx, preparation, false);
|
||||
} catch (error) {
|
||||
this.reportPreparationFailure(error, ctx, preparation);
|
||||
} finally {
|
||||
if (this.pendingPreparation === preparation) this.pendingPreparation = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
transform(payload: unknown, ctx: FastHost): Record<string, unknown> | undefined {
|
||||
if (this.disposed) return;
|
||||
if (!matchesDirectCodexSelection(payload, ctx) || !ctx.model) {
|
||||
@@ -224,7 +193,6 @@ export class FastController {
|
||||
invalidate(): void {
|
||||
this.pendingPreparation?.abortController.abort();
|
||||
this.pendingPreparation = undefined;
|
||||
this.cachedEligibility = undefined;
|
||||
this.modelTierActivity = undefined;
|
||||
}
|
||||
|
||||
@@ -236,6 +204,10 @@ export class FastController {
|
||||
}
|
||||
|
||||
private async enable(ctx: FastHost): Promise<void> {
|
||||
if (this.now() < this.retryAt) {
|
||||
ctx.ui.notify("The catalog service is rate limited. Try fast on again later.", "warning");
|
||||
return;
|
||||
}
|
||||
const preparation = this.startPreparation(ctx);
|
||||
try {
|
||||
const confirmed = await ctx.ui.confirm(
|
||||
@@ -244,8 +216,8 @@ export class FastController {
|
||||
{ signal: preparation.abortController.signal },
|
||||
);
|
||||
if (!this.canApplyPreparation(ctx, preparation) || !confirmed) return;
|
||||
const eligibility = await this.refreshEligibility(ctx, preparation, true);
|
||||
if (!this.canApplyPreparation(ctx, preparation) || eligibility !== "supported") return;
|
||||
const supported = await this.checkEligibility(ctx, preparation);
|
||||
if (!this.canApplyPreparation(ctx, preparation) || !supported) return;
|
||||
this.rememberChoice(ctx, "on");
|
||||
this.pendingPreparation = undefined;
|
||||
this.report(ctx);
|
||||
@@ -256,57 +228,21 @@ export class FastController {
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshEligibility(
|
||||
// Discovery belongs only to the explicit enable command, never to prompt submission or replay.
|
||||
private async checkEligibility(
|
||||
ctx: FastHost,
|
||||
preparation: EligibilityPreparation,
|
||||
force: boolean,
|
||||
): Promise<FastEligibility | undefined> {
|
||||
const signal = preparation.abortController.signal;
|
||||
const authScope = await this.readAuthScope(ctx.modelRegistry, signal);
|
||||
if (!this.canApplyPreparation(ctx, preparation)) return;
|
||||
const cached = this.cachedEligibility;
|
||||
if (
|
||||
!force &&
|
||||
cached?.modelKey === preparation.modelKey &&
|
||||
cached.authScope === authScope &&
|
||||
cached.expiresAt > this.now()
|
||||
)
|
||||
return cached.eligibility;
|
||||
|
||||
this.cachedEligibility = undefined;
|
||||
this.modelTierActivity = undefined;
|
||||
if (this.now() < this.retryAt) {
|
||||
ctx.ui.notify(
|
||||
"Fast mode suspended: the catalog service is rate limited. Try again later.",
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const catalog = await this.fetchCatalog(ctx.modelRegistry, signal);
|
||||
if (!this.canApplyPreparation(ctx, preparation)) return;
|
||||
if (catalog.authScope !== authScope) {
|
||||
ctx.ui.notify(
|
||||
"Fast mode suspended: authentication changed during discovery. Try fast on again.",
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const model = ctx.model;
|
||||
if (!model) return;
|
||||
const eligibility = codexFastEligibility(catalog.data, model.id);
|
||||
this.cachedEligibility = {
|
||||
modelKey: preparation.modelKey,
|
||||
authScope,
|
||||
eligibility,
|
||||
expiresAt: this.now() + CAPABILITY_TTL_MS,
|
||||
};
|
||||
if (eligibility !== "supported") {
|
||||
ctx.ui.notify(
|
||||
`Fast mode suspended: model eligibility is ${eligibility}. Preference unchanged.`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
return eligibility;
|
||||
): Promise<boolean> {
|
||||
const catalog = await this.fetchCatalog(ctx.modelRegistry, preparation.abortController.signal);
|
||||
if (!this.canApplyPreparation(ctx, preparation) || !ctx.model) return false;
|
||||
const eligibility = codexFastEligibility(catalog.data, ctx.model.id);
|
||||
if (eligibility === "supported") return true;
|
||||
const reason =
|
||||
eligibility === "unsupported"
|
||||
? "the model does not advertise priority processing"
|
||||
: "the catalog did not establish this model's priority support";
|
||||
ctx.ui.notify(`Could not enable fast mode: ${reason}. Preference unchanged.`, "warning");
|
||||
return false;
|
||||
}
|
||||
|
||||
private startPreparation(ctx: FastHost): EligibilityPreparation {
|
||||
@@ -351,14 +287,12 @@ export class FastController {
|
||||
preparation: EligibilityPreparation,
|
||||
): void {
|
||||
if (!this.canApplyPreparation(ctx, preparation)) return;
|
||||
this.cachedEligibility = undefined;
|
||||
this.modelTierActivity = undefined;
|
||||
if (error instanceof CodexAccountError && error.retryAfterMs !== undefined) {
|
||||
this.retryAt = this.now() + error.retryAfterMs;
|
||||
}
|
||||
let message = "Could not prepare Codex fast mode.";
|
||||
let message = "Could not enable Codex fast mode.";
|
||||
if (error instanceof CodexAccountError) message = error.message;
|
||||
ctx.ui.notify(`Fast mode suspended: ${message} Preference unchanged.`, "warning");
|
||||
ctx.ui.notify(`${message} Preference unchanged.`, "warning");
|
||||
}
|
||||
|
||||
private preferenceFor(selectedModelKey: string): FastPreference {
|
||||
@@ -377,21 +311,13 @@ export class FastController {
|
||||
this.modelTierActivity = undefined;
|
||||
}
|
||||
|
||||
private currentEligibility(selectedModelKey: string): FastEligibility {
|
||||
const cached = this.cachedEligibility;
|
||||
if (!cached || cached.modelKey !== selectedModelKey || cached.expiresAt <= this.now())
|
||||
return "unknown";
|
||||
return cached.eligibility;
|
||||
}
|
||||
|
||||
private requestPolicy(ctx: FastHost): FastRequestPolicy {
|
||||
if (!ctx.model || !isDirectCodexSelection(ctx)) return "outside scope";
|
||||
const selectedModelKey = modelKey(ctx.model);
|
||||
const preference = this.preferenceFor(selectedModelKey);
|
||||
if (preference === "inherit") return "untouched";
|
||||
if (preference === "off") return "default";
|
||||
if (this.pendingPreparation || this.currentEligibility(selectedModelKey) !== "supported")
|
||||
return "suspended";
|
||||
// A saved, confirmed choice is durable intent, not an expiring capability lease.
|
||||
return "priority";
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,6 @@ export default async function codex(pi: ExtensionAPI): Promise<void> {
|
||||
pi.on("input", async (event, ctx) => {
|
||||
if (event.streamingBehavior === undefined && ctx.isIdle()) {
|
||||
await controller.reconcile(ctx);
|
||||
await fast.prepare(ctx);
|
||||
}
|
||||
});
|
||||
pi.on("before_provider_request", (event, ctx) => fast.transform(event.payload, ctx));
|
||||
|
||||
Reference in new issue
Block a user