diff --git a/src/resources/extensions/gsd/model-router.ts b/src/resources/extensions/gsd/model-router.ts index fd76d53ca..fe8bdf0a5 100644 --- a/src/resources/extensions/gsd/model-router.ts +++ b/src/resources/extensions/gsd/model-router.ts @@ -114,6 +114,21 @@ export function resolveModelForComplexity( const configuredTier = getModelTier(configuredPrimary); const requestedTier = classification.tier; + // If the configured model is unknown (not in MODEL_CAPABILITY_TIER), + // honor the user's explicit choice — don't downgrade based on a guess. + // Unknown models default to "heavy" in getModelTier, which makes every + // standard/light unit get downgraded to tier_models, silently ignoring + // the user's configuration. (#2192) + if (!isKnownModel(configuredPrimary)) { + return { + modelId: configuredPrimary, + fallbacks: phaseConfig.fallbacks, + tier: requestedTier, + wasDowngraded: false, + reason: `configured model "${configuredPrimary}" is not in the known tier map — honoring explicit config`, + }; + } + // Downgrade-only: if requested tier >= configured tier, no change if (tierOrdinal(requestedTier) >= tierOrdinal(configuredTier)) { return { @@ -202,6 +217,16 @@ function getModelTier(modelId: string): ComplexityTier { return "heavy"; } +/** Check if a model ID has a known capability tier mapping. (#2192) */ +function isKnownModel(modelId: string): boolean { + const bareId = modelId.includes("/") ? modelId.split("/").pop()! : modelId; + if (MODEL_CAPABILITY_TIER[bareId]) return true; + for (const knownId of Object.keys(MODEL_CAPABILITY_TIER)) { + if (bareId.includes(knownId) || knownId.includes(bareId)) return true; + } + return false; +} + function findModelForTier( tier: ComplexityTier, config: DynamicRoutingConfig, diff --git a/src/resources/extensions/gsd/tests/model-router.test.ts b/src/resources/extensions/gsd/tests/model-router.test.ts index c7af7fcca..b22fce7fd 100644 --- a/src/resources/extensions/gsd/tests/model-router.test.ts +++ b/src/resources/extensions/gsd/tests/model-router.test.ts @@ -165,3 +165,43 @@ test("falls back to configured model when no light-tier model available", () => assert.equal(result.modelId, "claude-opus-4-6"); assert.equal(result.wasDowngraded, false); }); + +// ─── #2192: Unknown models honor explicit config ───────────────────────────── + +test("#2192: unknown model is not downgraded — respects user config", () => { + const config = { ...defaultRoutingConfig(), enabled: true }; + const result = resolveModelForComplexity( + makeClassification("light"), + { primary: "gpt-5.4", fallbacks: [] }, + config, + ["gpt-5.4", ...AVAILABLE_MODELS], + ); + assert.equal(result.modelId, "gpt-5.4", "unknown model should be used as-is"); + assert.equal(result.wasDowngraded, false, "should not be downgraded"); + assert.ok(result.reason.includes("not in the known tier map"), "reason should explain why"); +}); + +test("#2192: unknown model with provider prefix is not downgraded", () => { + const config = { ...defaultRoutingConfig(), enabled: true }; + const result = resolveModelForComplexity( + makeClassification("standard"), + { primary: "custom-provider/my-model-v3", fallbacks: [] }, + config, + ["custom-provider/my-model-v3", ...AVAILABLE_MODELS], + ); + assert.equal(result.modelId, "custom-provider/my-model-v3"); + assert.equal(result.wasDowngraded, false); +}); + +test("#2192: known model is still downgraded normally", () => { + const config = { ...defaultRoutingConfig(), enabled: true }; + // claude-opus-4-6 is known as "heavy" — a light request should downgrade + const result = resolveModelForComplexity( + makeClassification("light"), + { primary: "claude-opus-4-6", fallbacks: [] }, + config, + AVAILABLE_MODELS, + ); + assert.equal(result.wasDowngraded, true, "known heavy model should still be downgraded for light tasks"); + assert.notEqual(result.modelId, "claude-opus-4-6"); +});