feat: add fast mode toggle for OpenAI models

This commit is contained in:
Peter Steinberger
2026-03-12 23:30:58 +00:00
parent ddcaec89e9
commit d5bffcdeab
66 changed files with 990 additions and 36 deletions

View File

@@ -378,4 +378,42 @@ describe("executeSlashCommand directives", () => {
expect(result.content).toBe("Current verbose level: full.\nOptions: on, full, off.");
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
});
it("reports the current fast mode for bare /fast", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "sessions.list") {
return {
sessions: [row("agent:main:main", { fastMode: true })],
};
}
throw new Error(`unexpected method: ${method}`);
});
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"fast",
"",
);
expect(result.content).toBe("Current fast mode: on.\nOptions: status, on, off.");
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
});
it("patches fast mode for /fast on", async () => {
const request = vi.fn().mockResolvedValue({ ok: true });
const result = await executeSlashCommand(
{ request } as unknown as GatewayBrowserClient,
"agent:main:main",
"fast",
"on",
);
expect(result.content).toBe("Fast mode enabled.");
expect(request).toHaveBeenCalledWith("sessions.patch", {
key: "agent:main:main",
fastMode: true,
});
});
});

View File

@@ -63,6 +63,8 @@ export async function executeSlashCommand(
return await executeModel(client, sessionKey, args);
case "think":
return await executeThink(client, sessionKey, args);
case "fast":
return await executeFast(client, sessionKey, args);
case "verbose":
return await executeVerbose(client, sessionKey, args);
case "export":
@@ -252,6 +254,44 @@ async function executeVerbose(
}
}
async function executeFast(
client: GatewayBrowserClient,
sessionKey: string,
args: string,
): Promise<SlashCommandResult> {
const rawMode = args.trim().toLowerCase();
if (!rawMode || rawMode === "status") {
try {
const session = await loadCurrentSession(client, sessionKey);
return {
content: formatDirectiveOptions(
`Current fast mode: ${resolveCurrentFastMode(session)}.`,
"status, on, off",
),
};
} catch (err) {
return { content: `Failed to get fast mode: ${String(err)}` };
}
}
if (rawMode !== "on" && rawMode !== "off") {
return {
content: `Unrecognized fast mode "${args.trim()}". Valid levels: status, on, off.`,
};
}
try {
await client.request("sessions.patch", { key: sessionKey, fastMode: rawMode === "on" });
return {
content: `Fast mode ${rawMode === "on" ? "enabled" : "disabled"}.`,
action: "refresh",
};
} catch (err) {
return { content: `Failed to set fast mode: ${String(err)}` };
}
}
async function executeUsage(
client: GatewayBrowserClient,
sessionKey: string,
@@ -534,6 +574,10 @@ function resolveCurrentThinkingLevel(
});
}
function resolveCurrentFastMode(session: GatewaySessionRow | undefined): "on" | "off" {
return session?.fastMode === true ? "on" : "off";
}
function fmtTokens(n: number): string {
if (n >= 1_000_000) {
return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;

View File

@@ -23,4 +23,11 @@ describe("parseSlashCommand", () => {
args: "full",
});
});
it("parses fast commands", () => {
expect(parseSlashCommand("/fast:on")).toMatchObject({
command: { name: "fast" },
args: "on",
});
});
});

View File

@@ -88,6 +88,15 @@ export const SLASH_COMMANDS: SlashCommandDef[] = [
executeLocal: true,
argOptions: ["on", "off", "full"],
},
{
name: "fast",
description: "Toggle fast mode",
args: "<status|on|off>",
icon: "zap",
category: "model",
executeLocal: true,
argOptions: ["status", "on", "off"],
},
// ── Tools ──
{

View File

@@ -63,6 +63,7 @@ export async function patchSession(
patch: {
label?: string | null;
thinkingLevel?: string | null;
fastMode?: boolean | null;
verboseLevel?: string | null;
reasoningLevel?: string | null;
},
@@ -77,6 +78,9 @@ export async function patchSession(
if ("thinkingLevel" in patch) {
params.thinkingLevel = patch.thinkingLevel;
}
if ("fastMode" in patch) {
params.fastMode = patch.fastMode;
}
if ("verboseLevel" in patch) {
params.verboseLevel = patch.verboseLevel;
}

View File

@@ -379,6 +379,7 @@ export type GatewaySessionRow = {
systemSent?: boolean;
abortedLastRun?: boolean;
thinkingLevel?: string;
fastMode?: boolean;
verboseLevel?: string;
reasoningLevel?: string;
elevatedLevel?: string;
@@ -396,6 +397,7 @@ export type SessionsPatchResult = SessionsPatchResultBase<{
sessionId: string;
updatedAt?: number;
thinkingLevel?: string;
fastMode?: boolean;
verboseLevel?: string;
reasoningLevel?: string;
elevatedLevel?: string;

View File

@@ -60,7 +60,7 @@ describe("sessions view", () => {
await Promise.resolve();
const selects = container.querySelectorAll("select");
const verbose = selects[1] as HTMLSelectElement | undefined;
const verbose = selects[2] as HTMLSelectElement | undefined;
expect(verbose?.value).toBe("full");
expect(Array.from(verbose?.options ?? []).some((option) => option.value === "full")).toBe(true);
});
@@ -83,10 +83,32 @@ describe("sessions view", () => {
await Promise.resolve();
const selects = container.querySelectorAll("select");
const reasoning = selects[2] as HTMLSelectElement | undefined;
const reasoning = selects[3] as HTMLSelectElement | undefined;
expect(reasoning?.value).toBe("custom-mode");
expect(
Array.from(reasoning?.options ?? []).some((option) => option.value === "custom-mode"),
).toBe(true);
});
it("renders explicit fast mode without falling back to inherit", async () => {
const container = document.createElement("div");
render(
renderSessions(
buildProps(
buildResult({
key: "agent:main:main",
kind: "direct",
updatedAt: Date.now(),
fastMode: true,
}),
),
),
container,
);
await Promise.resolve();
const selects = container.querySelectorAll("select");
const fast = selects[1] as HTMLSelectElement | undefined;
expect(fast?.value).toBe("on");
});
});

View File

@@ -37,6 +37,7 @@ export type SessionsProps = {
patch: {
label?: string | null;
thinkingLevel?: string | null;
fastMode?: boolean | null;
verboseLevel?: string | null;
reasoningLevel?: string | null;
},
@@ -52,6 +53,11 @@ const VERBOSE_LEVELS = [
{ value: "on", label: "on" },
{ value: "full", label: "full" },
] as const;
const FAST_LEVELS = [
{ value: "", label: "inherit" },
{ value: "on", label: "on" },
{ value: "off", label: "off" },
] as const;
const REASONING_LEVELS = ["", "off", "on", "stream"] as const;
const PAGE_SIZES = [10, 25, 50, 100] as const;
@@ -306,6 +312,7 @@ export function renderSessions(props: SessionsProps) {
${sortHeader("updated", "Updated")}
${sortHeader("tokens", "Tokens")}
<th>Thinking</th>
<th>Fast</th>
<th>Verbose</th>
<th>Reasoning</th>
<th style="width: 60px;"></th>
@@ -316,7 +323,7 @@ export function renderSessions(props: SessionsProps) {
paginated.length === 0
? html`
<tr>
<td colspan="9" style="text-align: center; padding: 48px 16px; color: var(--muted)">
<td colspan="10" style="text-align: center; padding: 48px 16px; color: var(--muted)">
No sessions found.
</td>
</tr>
@@ -390,6 +397,8 @@ function renderRow(
const isBinaryThinking = isBinaryThinkingProvider(row.modelProvider);
const thinking = resolveThinkLevelDisplay(rawThinking, isBinaryThinking);
const thinkLevels = withCurrentOption(resolveThinkLevelOptions(row.modelProvider), thinking);
const fastMode = row.fastMode === true ? "on" : row.fastMode === false ? "off" : "";
const fastLevels = withCurrentLabeledOption(FAST_LEVELS, fastMode);
const verbose = row.verboseLevel ?? "";
const verboseLevels = withCurrentLabeledOption(VERBOSE_LEVELS, verbose);
const reasoning = row.reasoningLevel ?? "";
@@ -465,6 +474,23 @@ function renderRow(
)}
</select>
</td>
<td>
<select
?disabled=${disabled}
style="padding: 6px 10px; font-size: 13px; border: 1px solid var(--border); border-radius: var(--radius-sm); min-width: 90px;"
@change=${(e: Event) => {
const value = (e.target as HTMLSelectElement).value;
onPatch(row.key, { fastMode: value === "" ? null : value === "on" });
}}
>
${fastLevels.map(
(level) =>
html`<option value=${level.value} ?selected=${fastMode === level.value}>
${level.label}
</option>`,
)}
</select>
</td>
<td>
<select
?disabled=${disabled}