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:
parent
867f6558dc
commit
e1461f45b8
4 changed files with 117 additions and 16 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue