feat: add /gsd fast command and gate service tier icon to supported models (#1848) (#1862)

Add `/gsd fast [on|off|flex|status]` command for toggling OpenAI service
tiers, with `supportsServiceTier()` gating so the status bar icon only
appears on models that actually support service tiers (gpt-5.4 variants).

Fixes #1848

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher 2026-03-21 17:23:54 -04:00 committed by GitHub
parent 0dd7176af6
commit 7140ee0f53
9 changed files with 307 additions and 3 deletions

View file

@ -24,6 +24,7 @@ import { GLYPH, INDENT } from "../shared/mod.js";
import { computeProgressScore } from "./progress-score.js";
import { getActiveWorktreeName } from "./worktree-command.js";
import { loadEffectiveGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
import { resolveServiceTierIcon, getEffectiveServiceTier } from "./service-tier.js";
// ─── UAT Slice Extraction ─────────────────────────────────────────────────────
@ -460,6 +461,9 @@ export function updateProgressWidget(
// Pre-fetch last commit for display
refreshLastCommit(accessors.getBasePath());
// Cache the effective service tier at widget creation time (reads preferences)
const effectiveServiceTier = getEffectiveServiceTier();
ctx.ui.setWidget("gsd-progress", (tui, theme) => {
let pulseBright = true;
let cachedLines: string[] | undefined;
@ -572,9 +576,10 @@ export function updateProgressWidget(
// Model display — shown in context section, not stats
const modelId = cmdCtx?.model?.id ?? "";
const modelProvider = cmdCtx?.model?.provider ?? "";
const modelDisplay = modelProvider && modelId
const tierIcon = resolveServiceTierIcon(effectiveServiceTier, modelId);
const modelDisplay = (modelProvider && modelId
? `${modelProvider}/${modelId}`
: modelId;
: modelId) + (tierIcon ? ` ${tierIcon}` : "");
// ── Mode: off — return empty ──────────────────────────────────
if (widgetMode === "off") {

View file

@ -191,5 +191,18 @@ export function registerHooks(pi: ExtensionAPI): void {
pi.on("tool_execution_end", async (event) => {
markToolEnd(event.toolCallId);
});
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");
const tier = getEffectiveServiceTier();
if (!tier || !supportsServiceTier(modelId)) return;
const payload = event.payload as Record<string, unknown> | null;
if (!payload || typeof payload !== "object") return;
payload.service_tier = tier;
return payload;
});
}

View file

@ -14,7 +14,7 @@ export interface GsdCommandDefinition {
type CompletionMap = Record<string, readonly GsdCommandDefinition[]>;
export const GSD_COMMAND_DESCRIPTION =
"GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update";
"GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast";
export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
{ cmd: "help", desc: "Categorized command reference with descriptions" },
@ -64,6 +64,7 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
{ cmd: "start", desc: "Start a workflow template (bugfix, spike, feature, etc.)" },
{ cmd: "templates", desc: "List available workflow templates" },
{ cmd: "extensions", desc: "Manage extensions (list, enable, disable, info)" },
{ cmd: "fast", desc: "Toggle OpenAI service tier (on/off/flex/status)" },
];
const NESTED_COMPLETIONS: CompletionMap = {
@ -176,6 +177,12 @@ const NESTED_COMPLETIONS: CompletionMap = {
{ cmd: "disable", desc: "Disable an extension" },
{ cmd: "info", desc: "Show extension details" },
],
fast: [
{ cmd: "on", desc: "Priority tier (2x cost, faster)" },
{ cmd: "off", desc: "Disable service tier" },
{ cmd: "flex", desc: "Flex tier (0.5x cost, slower)" },
{ cmd: "status", desc: "Show current service tier setting" },
],
doctor: [
{ cmd: "fix", desc: "Auto-fix detected issues" },
{ cmd: "heal", desc: "AI-driven deep healing" },

View file

@ -52,6 +52,7 @@ export function showHelp(ctx: ExtensionCommandContext): void {
" /gsd keys API key manager [list|add|remove|test|rotate|doctor]",
" /gsd hooks Show post-unit hook configuration",
" /gsd extensions Manage extensions [list|enable|disable|info]",
" /gsd fast Toggle OpenAI service tier [on|off|flex|status]",
"",
"MAINTENANCE",
" /gsd doctor Diagnose and repair .gsd/ state [audit|fix|heal] [scope]",

View file

@ -172,6 +172,11 @@ Examples:
await handleUpdate(ctx);
return true;
}
if (trimmed === "fast" || trimmed.startsWith("fast ")) {
const { handleFast } = await import("../../service-tier.js");
await handleFast(trimmed.replace(/^fast\s*/, "").trim(), ctx);
return true;
}
if (trimmed === "extensions" || trimmed.startsWith("extensions ")) {
const { handleExtensions } = await import("../../commands-extensions.js");
await handleExtensions(trimmed.replace(/^extensions\s*/, "").trim(), ctx);

View file

@ -88,6 +88,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
"widget_mode",
"reactive_execution",
"github",
"service_tier",
]);
/** Canonical list of all dispatch unit types. */
@ -220,6 +221,8 @@ export interface GSDPreferences {
reactive_execution?: ReactiveExecutionConfig;
/** GitHub sync configuration. Opt-in: syncs GSD events to GitHub Issues, Milestones, and PRs. */
github?: GitHubSyncConfig;
/** OpenAI service tier preference. "priority" = 2x cost, faster. "flex" = 0.5x cost, slower. Only affects gpt-5.4 models. */
service_tier?: "priority" | "flex";
}
export interface LoadedGSDPreferences {

View file

@ -285,6 +285,7 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
github: (base.github || override.github)
? { ...(base.github ?? {}), ...(override.github ?? {}) } as import("../github-sync/types.js").GitHubSyncConfig
: undefined,
service_tier: override.service_tier ?? base.service_tier,
};
}

View file

@ -0,0 +1,171 @@
/**
* Service Tier gating, status formatting, icon resolution, and
* the /gsd fast command handler.
*
* Service tiers (priority/flex) are an OpenAI feature that only applies
* to gpt-5.4 variants. This module centralizes the model-gating logic
* so that icons, preferences, and the before_provider_request hook all
* use a single source of truth.
*/
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
import { existsSync, readFileSync } from "node:fs";
import { saveFile } from "./files.js";
import {
getGlobalGSDPreferencesPath,
loadEffectiveGSDPreferences,
loadGlobalGSDPreferences,
} from "./preferences.js";
import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
// ─── Types ───────────────────────────────────────────────────────────────────
export type ServiceTierSetting = "priority" | "flex" | undefined;
// ─── Gating ──────────────────────────────────────────────────────────────────
/**
* Returns true when the given model ID supports OpenAI service tiers.
* Currently only gpt-5.4 variants qualify.
*/
export function supportsServiceTier(modelId: string): boolean {
if (!modelId) return false;
// Strip provider prefix if present (e.g. "openai/gpt-5.4" → "gpt-5.4")
const bare = modelId.includes("/") ? modelId.split("/").pop()! : modelId;
return bare.startsWith("gpt-5.4");
}
// ─── Status Formatting ───────────────────────────────────────────────────────
/**
* Human-readable description of the current service tier setting.
*/
export function formatServiceTierStatus(tier: ServiceTierSetting): string {
if (!tier) {
return [
"Service tier: disabled",
"",
"Usage:",
" /gsd fast on Set to priority (2x cost, faster)",
" /gsd fast flex Set to flex (0.5x cost, slower)",
" /gsd fast off Disable service tier",
"",
"Only affects gpt-5.4 models.",
].join("\n");
}
const label = tier === "priority" ? "priority (2x cost, faster)" : "flex (0.5x cost, slower)";
return [
`Service tier: ${label}`,
"",
"Usage:",
" /gsd fast on Set to priority (2x cost, faster)",
" /gsd fast flex Set to flex (0.5x cost, slower)",
" /gsd fast off Disable service tier",
"",
"Only affects gpt-5.4 models.",
].join("\n");
}
// ─── Icon Resolution ─────────────────────────────────────────────────────────
/**
* Returns the appropriate icon for the active service tier and model.
* Returns empty string when the tier is inactive or the model doesn't
* support service tiers.
*/
export function resolveServiceTierIcon(tier: ServiceTierSetting, modelId: string): string {
if (!tier || !supportsServiceTier(modelId)) return "";
return tier === "priority" ? "⚡" : "💰";
}
// ─── Preference Read ─────────────────────────────────────────────────────────
/**
* Read the effective service_tier setting from preferences.
*/
export function getEffectiveServiceTier(): ServiceTierSetting {
const prefs = loadEffectiveGSDPreferences()?.preferences;
const raw = prefs?.service_tier;
if (raw === "priority" || raw === "flex") return raw;
return undefined;
}
// ─── Preference Write ────────────────────────────────────────────────────────
function extractBodyAfterFrontmatter(content: string): string | null {
const start = content.startsWith("---\n") ? 4 : content.startsWith("---\r\n") ? 5 : -1;
if (start === -1) return null;
const closingIdx = content.indexOf("\n---", start);
if (closingIdx === -1) return null;
const after = content.slice(closingIdx + 4);
return after.trim() ? after : null;
}
async function writeGlobalServiceTier(
ctx: ExtensionCommandContext,
tier: ServiceTierSetting,
): Promise<void> {
const path = getGlobalGSDPreferencesPath();
await ensurePreferencesFile(path, ctx, "global");
const existing = loadGlobalGSDPreferences();
const prefs: Record<string, unknown> = existing?.preferences ? { ...existing.preferences } : {};
prefs.version = prefs.version || 1;
if (tier) {
prefs.service_tier = tier;
} else {
delete prefs.service_tier;
}
const frontmatter = serializePreferencesToFrontmatter(prefs);
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
if (existsSync(path)) {
const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
if (preserved) body = preserved;
}
await saveFile(path, `---\n${frontmatter}---${body}`);
await ctx.waitForIdle();
await ctx.reload();
}
// ─── Command Handler ─────────────────────────────────────────────────────────
/**
* Handle `/gsd fast [on|off|flex|status]`.
*/
export async function handleFast(args: string, ctx: ExtensionCommandContext): Promise<void> {
const trimmed = args.trim().toLowerCase();
if (!trimmed || trimmed === "status") {
const tier = getEffectiveServiceTier();
ctx.ui.notify(formatServiceTierStatus(tier), "info");
return;
}
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");
return;
}
if (trimmed === "off") {
await writeGlobalServiceTier(ctx, 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");
return;
}
ctx.ui.notify(
"Usage: /gsd fast [on|off|flex|status]\n\n on Priority tier (2x cost, faster)\n off Disable service tier\n flex Flex tier (0.5x cost, slower)\n status Show current setting",
"warning",
);
}

View file

@ -0,0 +1,98 @@
import test, { describe } from "node:test";
import assert from "node:assert/strict";
import {
supportsServiceTier,
formatServiceTierStatus,
resolveServiceTierIcon,
type ServiceTierSetting,
} from "../service-tier.ts";
// ─── supportsServiceTier ─────────────────────────────────────────────────────
describe("supportsServiceTier", () => {
test("returns true for gpt-5.4", () => {
assert.equal(supportsServiceTier("gpt-5.4"), true);
});
test("returns true for gpt-5.4-pro", () => {
assert.equal(supportsServiceTier("gpt-5.4-pro"), true);
});
test("returns true for gpt-5.4-mini", () => {
assert.equal(supportsServiceTier("gpt-5.4-mini"), true);
});
test("returns true for openai/gpt-5.4 (provider-prefixed)", () => {
assert.equal(supportsServiceTier("openai/gpt-5.4"), true);
});
test("returns false for claude-opus-4-6", () => {
assert.equal(supportsServiceTier("claude-opus-4-6"), false);
});
test("returns false for gemini-2.5-pro", () => {
assert.equal(supportsServiceTier("gemini-2.5-pro"), false);
});
test("returns false for gpt-4o", () => {
assert.equal(supportsServiceTier("gpt-4o"), false);
});
test("returns false for empty string", () => {
assert.equal(supportsServiceTier(""), false);
});
});
// ─── formatServiceTierStatus ─────────────────────────────────────────────────
describe("formatServiceTierStatus", () => {
test("shows disabled when service_tier is undefined", () => {
const output = formatServiceTierStatus(undefined);
assert.ok(output.includes("disabled"), `Expected 'disabled' in: ${output}`);
});
test("shows priority when set to priority", () => {
const output = formatServiceTierStatus("priority");
assert.ok(output.includes("priority"), `Expected 'priority' in: ${output}`);
});
test("shows flex when set to flex", () => {
const output = formatServiceTierStatus("flex");
assert.ok(output.includes("flex"), `Expected 'flex' in: ${output}`);
});
});
// ─── resolveServiceTierIcon ──────────────────────────────────────────────────
describe("resolveServiceTierIcon", () => {
test("returns lightning bolt for priority tier on supported model", () => {
const icon = resolveServiceTierIcon("priority", "gpt-5.4");
assert.equal(icon, "⚡");
});
test("returns money icon for flex tier on supported model", () => {
const icon = resolveServiceTierIcon("flex", "gpt-5.4");
assert.equal(icon, "💰");
});
test("returns empty string when tier is set but model does not support it", () => {
const icon = resolveServiceTierIcon("priority", "claude-opus-4-6");
assert.equal(icon, "");
});
test("returns empty string when tier is undefined", () => {
const icon = resolveServiceTierIcon(undefined, "gpt-5.4");
assert.equal(icon, "");
});
test("returns empty string when both tier and model are unsupported", () => {
const icon = resolveServiceTierIcon(undefined, "claude-opus-4-6");
assert.equal(icon, "");
});
test("returns empty string when model is empty", () => {
const icon = resolveServiceTierIcon("priority", "");
assert.equal(icon, "");
});
});