fix(gsd): harden flat-rate routing guard against alias/resolution gaps

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
This commit is contained in:
Jeremy 2026-04-05 13:09:44 -05:00
parent 9ab675a843
commit 3a1e9e3416
2 changed files with 26 additions and 3 deletions

View file

@ -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<T extends { id: string; provider: string }>(
/**
* 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());
}

View file

@ -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);
});