service-tier: add "off" preference value to fully disable feature

Adds an explicit disable state (service_tier: "off" in PREFERENCES.md)
that short-circuits every service-tier surface:

- No setStatus("sf-fast", …) footer events — RPC traffic stops, not
  just the stderr filter masking it.
- No service_tier field ever injected into before_provider_request
  payloads, regardless of model.
- /sf fast on and /sf fast flex refuse to write a tier while "off" is
  set, instructing the user to clear the preference first.
- /sf fast status shows "(service_tier: \"off\" in preferences)" so
  the explicit disable is visible at a glance.

Rationale: setups that never run gpt-5.4 (Claude / Kimi / MiniMax /
GLM / Gemini-only shops) have no use for the feature. "off" lets them
fully turn it off rather than relying on model-support gates to
silence it.

6 regression tests added in service-tier.test.ts covering the new
isServiceTierDisabled export, hook short-circuit ordering, and the
/sf fast command refusal. 52 / 52 service-tier tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-04-19 07:31:14 +02:00
parent 867f6558dc
commit e1461f45b8
4 changed files with 117 additions and 16 deletions

View file

@ -34,7 +34,11 @@ import { initializeLearningRuntime, resetLearningRuntime, selectLearnedModel } f
let isFirstSession = true;
async function syncServiceTierStatus(ctx: ExtensionContext): Promise<void> {
const { getEffectiveServiceTier, formatServiceTierFooterStatus } = await import("../service-tier.js");
const { getEffectiveServiceTier, formatServiceTierFooterStatus, isServiceTierDisabled } = await import("../service-tier.js");
// Skip the footer event entirely when the feature is explicitly disabled —
// no setStatus call, no RPC traffic, no leak into headless stderr even if
// the TUI_FOOTER_STATUS_KEYS filter is bypassed.
if (isServiceTierDisabled()) return;
ctx.ui.setStatus("sf-fast", formatServiceTierFooterStatus(getEffectiveServiceTier(), ctx.model?.id));
}
@ -454,7 +458,10 @@ export function registerHooks(pi: ExtensionAPI): void {
// ── Service Tier ────────────────────────────────────────────────────
const modelId = event.model?.id;
if (!modelId) return payload;
const { getEffectiveServiceTier, supportsServiceTier } = await import("../service-tier.js");
const { getEffectiveServiceTier, supportsServiceTier, isServiceTierDisabled } = await import("../service-tier.js");
// Short-circuit on explicit disable — never inject service_tier on any
// setup that has opted out, regardless of model.
if (isServiceTierDisabled()) return payload;
const tier = getEffectiveServiceTier();
if (!tier || !supportsServiceTier(modelId)) return payload;
payload.service_tier = tier;

View file

@ -324,8 +324,17 @@ export interface SFPreferences {
gate_evaluation?: GateEvaluationConfig;
/** GitHub sync configuration. Opt-in: syncs SF 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";
/**
* OpenAI service tier preference. Only affects gpt-5.4 models.
* - "priority" 2x cost, faster.
* - "flex" 0.5x cost, slower.
* - "off" explicitly disabled: no footer status event, no
* service_tier field in outbound requests. Use this on
* setups that will never run gpt-5.4 so the feature is
* fully dormant.
* - undefined not configured (default).
*/
service_tier?: "priority" | "flex" | "off";
/** Opt-in: search existing issues and PRs before filing from /sf forensics. Uses additional AI tokens. */
forensics_dedup?: boolean;
/** Opt-in: show per-prompt and cumulative session token cost in the footer. Default: false. */

View file

@ -22,6 +22,7 @@ import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./comm
// ─── Types ───────────────────────────────────────────────────────────────────
export type ServiceTierSetting = "priority" | "flex" | undefined;
export type ServiceTierPref = "priority" | "flex" | "off" | undefined;
const SERVICE_TIER_SCOPE_NOTE = "Only affects gpt-5.4 models, regardless of provider.";
@ -57,8 +58,12 @@ export function supportsServiceTier(modelId: string): boolean {
*/
export function formatServiceTierStatus(tier: ServiceTierSetting): string {
if (!tier) {
const explicitlyDisabled = isServiceTierDisabled();
const header = explicitlyDisabled
? "Service tier: disabled (service_tier: \"off\" in preferences)"
: "Service tier: disabled";
return [
"Service tier: disabled",
header,
"",
"Usage:",
" /sf fast on Set to priority (2x cost, faster)",
@ -106,6 +111,9 @@ export function resolveServiceTierIcon(tier: ServiceTierSetting, modelId: string
/**
* Read the effective service_tier setting from preferences.
* Returns undefined for both "explicitly disabled" (`"off"`) and "not
* configured"; callers that need to distinguish the two should use
* {@link isServiceTierDisabled}.
*/
export function getEffectiveServiceTier(): ServiceTierSetting {
const prefs = loadEffectiveSFPreferences()?.preferences;
@ -114,6 +122,17 @@ export function getEffectiveServiceTier(): ServiceTierSetting {
return undefined;
}
/**
* True when the user has explicitly set `service_tier: "off"` in preferences.
* Callers that register UI/hooks for the feature should skip registration
* entirely in this case not just no-op their behavior. That keeps the
* feature fully dormant on setups that will never run a gpt-5.4 model.
*/
export function isServiceTierDisabled(): boolean {
const prefs = loadEffectiveSFPreferences()?.preferences;
return (prefs?.service_tier as ServiceTierPref) === "off";
}
// ─── Preference Write ────────────────────────────────────────────────────────
function extractBodyAfterFrontmatter(content: string): string | null {
@ -168,10 +187,21 @@ export async function handleFast(args: string, ctx: ExtensionCommandContext): Pr
return;
}
if (trimmed === "on") {
await writeGlobalServiceTier(ctx, "priority");
ctx.ui.setStatus("sf-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");
if (trimmed === "on" || trimmed === "flex") {
if (isServiceTierDisabled()) {
ctx.ui.notify(
"Service tier is explicitly disabled (service_tier: \"off\" in preferences). Remove that line before enabling.",
"warning",
);
return;
}
const nextTier: ServiceTierSetting = trimmed === "on" ? "priority" : "flex";
await writeGlobalServiceTier(ctx, nextTier);
ctx.ui.setStatus("sf-fast", formatServiceTierFooterStatus(nextTier, ctx.model?.id));
const label = nextTier === "priority"
? "priority (2x cost, faster responses)"
: "flex (0.5x cost, slower responses)";
ctx.ui.notify(`Service tier set to ${label}. Only affects gpt-5.4 models, regardless of provider.`, "info");
return;
}
@ -182,13 +212,6 @@ export async function handleFast(args: string, ctx: ExtensionCommandContext): Pr
return;
}
if (trimmed === "flex") {
await writeGlobalServiceTier(ctx, "flex");
ctx.ui.setStatus("sf-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;
}
ctx.ui.notify(
"Usage: /sf 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

@ -125,3 +125,65 @@ describe("resolveServiceTierIcon", () => {
assert.equal(icon, "");
});
});
// ─── Disable via "off" preference ────────────────────────────────────────────
//
// These are structural tests against the source so they don't depend on the
// effective-preferences loader behaving the same way in a test env as in a
// real SF install. The runtime behavior is covered by hook integration paths.
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const serviceTierSrc = readFileSync(join(__dirname, "..", "service-tier.ts"), "utf-8");
const hooksSrc = readFileSync(join(__dirname, "..", "bootstrap", "register-hooks.ts"), "utf-8");
describe("service_tier: \"off\" disable", () => {
test("isServiceTierDisabled is exported and keys off the preference", () => {
assert.match(serviceTierSrc, /export function isServiceTierDisabled\(\)/);
assert.match(
serviceTierSrc,
/isServiceTierDisabled[\s\S]*?prefs\?\.service_tier[\s\S]*?===\s*"off"/,
);
});
test("getEffectiveServiceTier does not treat \"off\" as a tier", () => {
// getEffectiveServiceTier only returns priority|flex — "off" collapses to undefined.
assert.match(
serviceTierSrc,
/export function getEffectiveServiceTier\(\)[\s\S]*?if \(raw === "priority" \|\| raw === "flex"\) return raw;\s*return undefined;/,
);
});
test("syncServiceTierStatus skips setStatus when disabled", () => {
assert.match(
hooksSrc,
/async function syncServiceTierStatus\([\s\S]*?if \(isServiceTierDisabled\(\)\) return;[\s\S]*?setStatus\("sf-fast"/,
);
});
test("before_provider_request hook short-circuits when disabled", () => {
// Must check isServiceTierDisabled before the normal tier/support gate.
const hook = hooksSrc.slice(hooksSrc.indexOf("// ── Service Tier ──"));
assert.match(hook, /if \(isServiceTierDisabled\(\)\) return payload/);
assert.ok(
hook.indexOf("isServiceTierDisabled") < hook.indexOf("getEffectiveServiceTier()"),
"disable check must run before tier resolution",
);
});
test("/sf fast on refuses when explicitly disabled", () => {
// handleFast("on") must bail out with a warning rather than silently
// overwriting the explicit "off" pref.
assert.match(
serviceTierSrc,
/if \(trimmed === "on" \|\| trimmed === "flex"\)[\s\S]*?if \(isServiceTierDisabled\(\)\)[\s\S]*?"warning"/,
);
});
test("preferences-types declares \"off\" as a valid service_tier value", () => {
const typesSrc = readFileSync(join(__dirname, "..", "preferences-types.ts"), "utf-8");
assert.match(typesSrc, /service_tier\?:\s*"priority"\s*\|\s*"flex"\s*\|\s*"off"/);
});
});