fix: allow variant override even with agent model config (#3163)

model-selection.ts now separates model selection from variant/reasoning
tier, so agent model overrides don't lock the variant.

34 tests pass, 5009 total, tsc clean.

Closes #3163
This commit is contained in:
YeonGyu-Kim
2026-04-07 15:24:19 +09:00
parent be562905d6
commit ee8410ce03
3 changed files with 103 additions and 1 deletions
@@ -197,7 +197,7 @@ describe("resolveCategoryExecution", () => {
if (!result.actualModel || !result.categoryModel) {
throw new Error("Expected resolved model and category model")
}
expect(result.actualModel).toBe("openai/gpt-5.4 high")
expect(result.actualModel).toBe("openai/gpt-5.4")
expect(result.categoryModel).toEqual({
providerID: "openai",
modelID: "gpt-5.4",
@@ -254,6 +254,100 @@ describe("resolveModelForDelegateTask", () => {
})
})
describe("#given user model override includes variant syntax", () => {
describe("#when userModel contains space-separated variant", () => {
test("#then extracts the variant and returns the base model separately", () => {
const result = resolveModelForDelegateTask({
userModel: "openai/gpt-5.4 high",
categoryDefaultModel: "anthropic/claude-sonnet-4-6",
fallbackChain: [
{ providers: ["anthropic"], model: "claude-sonnet-4-6" },
],
availableModels: new Set(["openai/gpt-5.4"]),
})
expect(result).toEqual({ model: "openai/gpt-5.4", variant: "high" })
})
})
describe("#when userModel contains parenthesized variant", () => {
test("#then extracts the variant and returns the base model separately", () => {
const result = resolveModelForDelegateTask({
userModel: "openai/gpt-5.4(max)",
categoryDefaultModel: "anthropic/claude-sonnet-4-6",
availableModels: new Set(),
})
expect(result).toEqual({ model: "openai/gpt-5.4", variant: "max" })
})
})
describe("#when userModel has no variant syntax", () => {
test("#then returns the model without a variant (backward compat)", () => {
const result = resolveModelForDelegateTask({
userModel: "openai/gpt-5.4",
availableModels: new Set(),
})
expect(result).toEqual({ model: "openai/gpt-5.4" })
})
})
describe("#when userModel has a non-variant suffix (e.g. -high in model name)", () => {
test("#then preserves the full model name without extracting a variant", () => {
const result = resolveModelForDelegateTask({
userModel: "new-api-openai/gpt-5.4-high",
availableModels: new Set(),
})
expect(result).toEqual({ model: "new-api-openai/gpt-5.4-high" })
})
})
})
describe("#given user-configured category model includes variant syntax", () => {
beforeEach(() => {
hasConnectedProvidersSpy = spyOn(connectedProvidersCache, "hasConnectedProvidersCache").mockReturnValue(true)
hasProviderModelsSpy = spyOn(connectedProvidersCache, "hasProviderModelsCache").mockReturnValue(true)
})
describe("#when categoryDefaultModel with isUserConfiguredCategoryModel contains a space-separated variant", () => {
test("#then extracts the variant and returns the base model separately", () => {
const result = resolveModelForDelegateTask({
categoryDefaultModel: "openai/gpt-5.4 medium",
isUserConfiguredCategoryModel: true,
availableModels: new Set(["openai/gpt-5.4"]),
})
expect(result).toEqual({ model: "openai/gpt-5.4", variant: "medium" })
})
})
describe("#when categoryDefaultModel with isUserConfiguredCategoryModel contains a parenthesized variant", () => {
test("#then extracts the variant and returns the base model separately", () => {
const result = resolveModelForDelegateTask({
categoryDefaultModel: "openai/gpt-5.4(xhigh)",
isUserConfiguredCategoryModel: true,
availableModels: new Set(),
})
expect(result).toEqual({ model: "openai/gpt-5.4", variant: "xhigh" })
})
})
describe("#when categoryDefaultModel with isUserConfiguredCategoryModel has no variant", () => {
test("#then returns the model without a variant (backward compat)", () => {
const result = resolveModelForDelegateTask({
categoryDefaultModel: "new-api-openai/gpt-5.4-high",
isUserConfiguredCategoryModel: true,
availableModels: new Set(["openai/gpt-5.4"]),
})
expect(result).toEqual({ model: "new-api-openai/gpt-5.4-high" })
})
})
})
describe("#given only connected providers cache exists (no provider-models cache)", () => {
beforeEach(() => {
hasConnectedProvidersSpy = spyOn(connectedProvidersCache, "hasConnectedProvidersCache").mockReturnValue(true)
@@ -56,6 +56,10 @@ export function resolveModelForDelegateTask(input: {
}): { model: string; variant?: string; fallbackEntry?: FallbackEntry; matchedFallback?: boolean } | { skipped: true } | undefined {
const userModel = normalizeModel(input.userModel)
if (userModel) {
const parsed = parseUserFallbackModel(userModel)
if (parsed?.variant) {
return { model: parsed.baseModel, variant: parsed.variant }
}
return { model: userModel }
}
@@ -75,6 +79,10 @@ export function resolveModelForDelegateTask(input: {
log("[resolveModelForDelegateTask] using user-configured category model (bypass validation)", {
categoryDefaultModel: categoryDefault,
})
const parsed = parseUserFallbackModel(categoryDefault)
if (parsed?.variant) {
return { model: parsed.baseModel, variant: parsed.variant }
}
return { model: categoryDefault }
}