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:
@@ -197,7 +197,7 @@ describe("resolveCategoryExecution", () => {
|
|||||||
if (!result.actualModel || !result.categoryModel) {
|
if (!result.actualModel || !result.categoryModel) {
|
||||||
throw new Error("Expected resolved model and category model")
|
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({
|
expect(result.categoryModel).toEqual({
|
||||||
providerID: "openai",
|
providerID: "openai",
|
||||||
modelID: "gpt-5.4",
|
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)", () => {
|
describe("#given only connected providers cache exists (no provider-models cache)", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
hasConnectedProvidersSpy = spyOn(connectedProvidersCache, "hasConnectedProvidersCache").mockReturnValue(true)
|
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 {
|
}): { model: string; variant?: string; fallbackEntry?: FallbackEntry; matchedFallback?: boolean } | { skipped: true } | undefined {
|
||||||
const userModel = normalizeModel(input.userModel)
|
const userModel = normalizeModel(input.userModel)
|
||||||
if (userModel) {
|
if (userModel) {
|
||||||
|
const parsed = parseUserFallbackModel(userModel)
|
||||||
|
if (parsed?.variant) {
|
||||||
|
return { model: parsed.baseModel, variant: parsed.variant }
|
||||||
|
}
|
||||||
return { model: userModel }
|
return { model: userModel }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +79,10 @@ export function resolveModelForDelegateTask(input: {
|
|||||||
log("[resolveModelForDelegateTask] using user-configured category model (bypass validation)", {
|
log("[resolveModelForDelegateTask] using user-configured category model (bypass validation)", {
|
||||||
categoryDefaultModel: categoryDefault,
|
categoryDefaultModel: categoryDefault,
|
||||||
})
|
})
|
||||||
|
const parsed = parseUserFallbackModel(categoryDefault)
|
||||||
|
if (parsed?.variant) {
|
||||||
|
return { model: parsed.baseModel, variant: parsed.variant }
|
||||||
|
}
|
||||||
return { model: categoryDefault }
|
return { model: categoryDefault }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user