diff --git a/src/resources/extensions/gsd/bootstrap/register-hooks.ts b/src/resources/extensions/gsd/bootstrap/register-hooks.ts index 1ff2452f9..99fa9cc9c 100644 --- a/src/resources/extensions/gsd/bootstrap/register-hooks.ts +++ b/src/resources/extensions/gsd/bootstrap/register-hooks.ts @@ -20,21 +20,27 @@ import { saveActivityLog } from "../activity-log.js"; // printed it before the TUI launched. Only re-print on /clear (subsequent sessions). let isFirstSession = true; +async function syncServiceTierStatus(ctx: ExtensionContext): Promise { + const { getEffectiveServiceTier, formatServiceTierFooterStatus } = await import("../service-tier.js"); + ctx.ui.setStatus("gsd-fast", formatServiceTierFooterStatus(getEffectiveServiceTier(), ctx.model?.id)); +} + export function registerHooks(pi: ExtensionAPI): void { pi.on("session_start", async (_event, ctx) => { resetWriteGateState(); resetToolCallLoopGuard(); + await syncServiceTierStatus(ctx); if (isFirstSession) { isFirstSession = false; } else { try { const gsdBinPath = process.env.GSD_BIN_PATH; if (gsdBinPath) { - const { dirname } = await import('node:path'); + const { dirname } = await import("node:path"); const { printWelcomeScreen } = await import( - join(dirname(gsdBinPath), 'welcome-screen.js') + join(dirname(gsdBinPath), "welcome-screen.js") ) as { printWelcomeScreen: (opts: { version: string; modelName?: string; provider?: string }) => void }; - printWelcomeScreen({ version: process.env.GSD_VERSION || '0.0.0' }); + printWelcomeScreen({ version: process.env.GSD_VERSION || "0.0.0" }); } } catch { /* non-fatal */ } } @@ -192,8 +198,11 @@ export function registerHooks(pi: ExtensionAPI): void { markToolEnd(event.toolCallId); }); + pi.on("model_select", async (_event, ctx) => { + await syncServiceTierStatus(ctx); + }); + pi.on("before_provider_request", async (event) => { - if (!isAutoActive()) return; const modelId = event.model?.id; if (!modelId) return; const { getEffectiveServiceTier, supportsServiceTier } = await import("../service-tier.js"); @@ -205,4 +214,3 @@ export function registerHooks(pi: ExtensionAPI): void { return payload; }); } - diff --git a/src/resources/extensions/gsd/service-tier.ts b/src/resources/extensions/gsd/service-tier.ts index 7e2f4613a..9ef836dc6 100644 --- a/src/resources/extensions/gsd/service-tier.ts +++ b/src/resources/extensions/gsd/service-tier.ts @@ -23,6 +23,8 @@ import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./comm export type ServiceTierSetting = "priority" | "flex" | undefined; +const SERVICE_TIER_SCOPE_NOTE = "Only affects gpt-5.4 models, regardless of provider."; + // ─── Gating ────────────────────────────────────────────────────────────────── /** @@ -51,7 +53,7 @@ export function formatServiceTierStatus(tier: ServiceTierSetting): string { " /gsd fast flex Set to flex (0.5x cost, slower)", " /gsd fast off Disable service tier", "", - "Only affects gpt-5.4 models.", + SERVICE_TIER_SCOPE_NOTE, ].join("\n"); } @@ -64,10 +66,18 @@ export function formatServiceTierStatus(tier: ServiceTierSetting): string { " /gsd fast flex Set to flex (0.5x cost, slower)", " /gsd fast off Disable service tier", "", - "Only affects gpt-5.4 models.", + SERVICE_TIER_SCOPE_NOTE, ].join("\n"); } +export function formatServiceTierFooterStatus( + tier: ServiceTierSetting, + modelId: string | undefined, +): string | undefined { + if (!tier || !modelId || !supportsServiceTier(modelId)) return undefined; + return tier === "priority" ? "fast: ⚡ priority" : "fast: 💰 flex"; +} + // ─── Icon Resolution ───────────────────────────────────────────────────────── /** @@ -148,19 +158,22 @@ export async function handleFast(args: string, ctx: ExtensionCommandContext): Pr if (trimmed === "on") { await writeGlobalServiceTier(ctx, "priority"); - ctx.ui.notify("Service tier set to priority (2x cost, faster responses). Only affects gpt-5.4 models.", "info"); + ctx.ui.setStatus("gsd-fast", formatServiceTierFooterStatus("priority", ctx.model?.id)); + ctx.ui.notify("Service tier set to priority (2x cost, faster responses). Only affects gpt-5.4 models, regardless of provider.", "info"); return; } if (trimmed === "off") { await writeGlobalServiceTier(ctx, undefined); + ctx.ui.setStatus("gsd-fast", undefined); ctx.ui.notify("Service tier disabled.", "info"); return; } if (trimmed === "flex") { await writeGlobalServiceTier(ctx, "flex"); - ctx.ui.notify("Service tier set to flex (0.5x cost, slower responses). Only affects gpt-5.4 models.", "info"); + ctx.ui.setStatus("gsd-fast", formatServiceTierFooterStatus("flex", ctx.model?.id)); + ctx.ui.notify("Service tier set to flex (0.5x cost, slower responses). Only affects gpt-5.4 models, regardless of provider.", "info"); return; } diff --git a/src/resources/extensions/gsd/tests/service-tier.test.ts b/src/resources/extensions/gsd/tests/service-tier.test.ts index ff6d0b684..2192c9aa7 100644 --- a/src/resources/extensions/gsd/tests/service-tier.test.ts +++ b/src/resources/extensions/gsd/tests/service-tier.test.ts @@ -4,8 +4,8 @@ import assert from "node:assert/strict"; import { supportsServiceTier, formatServiceTierStatus, + formatServiceTierFooterStatus, resolveServiceTierIcon, - type ServiceTierSetting, } from "../service-tier.ts"; // ─── supportsServiceTier ───────────────────────────────────────────────────── @@ -27,6 +27,14 @@ describe("supportsServiceTier", () => { assert.equal(supportsServiceTier("openai/gpt-5.4"), true); }); + test("returns true for vibeproxy-openai/gpt-5.4 (proxy provider-prefixed)", () => { + assert.equal(supportsServiceTier("vibeproxy-openai/gpt-5.4"), true); + }); + + test("returns false for provider-only identifier without gpt-5.4 model suffix", () => { + assert.equal(supportsServiceTier("vibeproxy-openai"), false); + }); + test("returns false for claude-opus-4-6", () => { assert.equal(supportsServiceTier("claude-opus-4-6"), false); }); @@ -52,6 +60,11 @@ describe("formatServiceTierStatus", () => { assert.ok(output.includes("disabled"), `Expected 'disabled' in: ${output}`); }); + test("mentions provider-agnostic model gating", () => { + const output = formatServiceTierStatus("priority"); + assert.ok(output.includes("regardless of provider"), `Expected provider note in: ${output}`); + }); + test("shows priority when set to priority", () => { const output = formatServiceTierStatus("priority"); assert.ok(output.includes("priority"), `Expected 'priority' in: ${output}`); @@ -63,6 +76,22 @@ describe("formatServiceTierStatus", () => { }); }); +// ─── formatServiceTierFooterStatus ─────────────────────────────────────────── + +describe("formatServiceTierFooterStatus", () => { + test("returns priority footer status for supported model", () => { + assert.equal(formatServiceTierFooterStatus("priority", "vibeproxy-openai/gpt-5.4"), "fast: ⚡ priority"); + }); + + test("returns undefined for unsupported model", () => { + assert.equal(formatServiceTierFooterStatus("priority", "claude-opus-4-6"), undefined); + }); + + test("returns undefined when tier is disabled", () => { + assert.equal(formatServiceTierFooterStatus(undefined, "gpt-5.4"), undefined); + }); +}); + // ─── resolveServiceTierIcon ────────────────────────────────────────────────── describe("resolveServiceTierIcon", () => {