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) {
|
||||
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 }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user