From 3a1e9e3416a6707f78608be84efaec59f10e6dcc Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 5 Apr 2026 13:09:44 -0500 Subject: [PATCH] fix(gsd): harden flat-rate routing guard against alias/resolution gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flat-rate provider guard from #3552 can fail open in two scenarios: 1. Provider alias mismatch — isFlatRateProvider only matched the exact string "github-copilot", but "copilot" appears as a provider alias in the codebase. Case variations could also bypass the check. Fix: add "copilot" alias and lowercase input before set membership. 2. Unresolved primary model — when resolveModelId returns undefined (stale model ID, registry mismatch), the guard was skipped entirely, allowing dynamic routing to downgrade models on a flat-rate backend. Fix: fall back to autoModeStartModel.provider and ctx.model.provider when primary resolution fails, disabling routing if either indicates a flat-rate provider. Ref: #3453 --- .../extensions/gsd/auto-model-selection.ts | 19 ++++++++++++++++--- .../gsd/tests/flat-rate-routing-guard.test.ts | 10 ++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/resources/extensions/gsd/auto-model-selection.ts b/src/resources/extensions/gsd/auto-model-selection.ts index 6b8aabe70..ad26edca8 100644 --- a/src/resources/extensions/gsd/auto-model-selection.ts +++ b/src/resources/extensions/gsd/auto-model-selection.ts @@ -77,9 +77,20 @@ export async function selectAndApplyModel( // Disable routing for flat-rate providers like GitHub Copilot (#3453). // All models cost the same per request, so downgrading to a cheaper // model provides no cost benefit — it only degrades quality. + // Fail-closed: if primary model can't be resolved, fall back to + // provider-level signals rather than allowing unwanted downgrades. if (routingConfig.enabled) { const primaryModel = resolveModelId(modelConfig.primary, availableModels, ctx.model?.provider); - if (primaryModel && isFlatRateProvider(primaryModel.provider)) { + if (primaryModel) { + if (isFlatRateProvider(primaryModel.provider)) { + routingConfig.enabled = false; + } + } else if ( + (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider)) + || (ctx.model?.provider && isFlatRateProvider(ctx.model.provider)) + ) { + // Primary model unresolvable but provider signals indicate flat-rate — + // disable routing to prevent quality degradation. routingConfig.enabled = false; } } @@ -337,9 +348,11 @@ export function resolveModelId( /** * Flat-rate providers charge the same per request regardless of model. * Dynamic routing provides no cost benefit — it only degrades quality (#3453). + * Uses case-insensitive matching with alias support to prevent fail-open on + * provider naming variations (e.g. "copilot" vs "github-copilot"). */ -const FLAT_RATE_PROVIDERS = new Set(["github-copilot"]); +const FLAT_RATE_PROVIDERS = new Set(["github-copilot", "copilot"]); export function isFlatRateProvider(provider: string): boolean { - return FLAT_RATE_PROVIDERS.has(provider); + return FLAT_RATE_PROVIDERS.has(provider.toLowerCase()); } diff --git a/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts b/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts index a02b0b9f6..64a93608f 100644 --- a/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +++ b/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts @@ -14,6 +14,16 @@ describe("flat-rate provider routing guard (#3453)", () => { assert.equal(isFlatRateProvider("github-copilot"), true); }); + test("isFlatRateProvider returns true for copilot alias", () => { + assert.equal(isFlatRateProvider("copilot"), true); + }); + + test("isFlatRateProvider is case-insensitive", () => { + assert.equal(isFlatRateProvider("GitHub-Copilot"), true); + assert.equal(isFlatRateProvider("GITHUB-COPILOT"), true); + assert.equal(isFlatRateProvider("Copilot"), true); + }); + test("isFlatRateProvider returns false for anthropic", () => { assert.equal(isFlatRateProvider("anthropic"), false); });