Merge pull request #3982 from jas32096/fix/category-fallback-ignored-when-primary-set
fix(delegate-task): honor user fallback_models when category primary is unreachable
This commit is contained in:
@@ -170,6 +170,50 @@ describe("resolveModelForDelegateTask", () => {
|
|||||||
expect(result).toEqual({ model: "openai/gpt-5.2", variant: "medium", matchedFallback: true })
|
expect(result).toEqual({ model: "openai/gpt-5.2", variant: "medium", matchedFallback: true })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("#when user primary model is unreachable and user fallback_models are provided", () => {
|
||||||
|
test("#then promotes the first reachable user fallback (regression: bug where fallback_models were ignored when userModel set)", () => {
|
||||||
|
const result = resolveModelForDelegateTask({
|
||||||
|
userModel: "opencode/gemini-3.1-pro high",
|
||||||
|
userFallbackModels: [
|
||||||
|
"amazon-bedrock/us.anthropic.claude-opus-4-7 max",
|
||||||
|
"opencode/claude-opus-4-7 max",
|
||||||
|
"openai/gpt-5.5",
|
||||||
|
],
|
||||||
|
availableModels: new Set([
|
||||||
|
"openai/gpt-5.5",
|
||||||
|
"openai/gpt-5.5-pro",
|
||||||
|
"amazon-bedrock/us.anthropic.claude-opus-4-7",
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
model: "amazon-bedrock/us.anthropic.claude-opus-4-7",
|
||||||
|
variant: "max",
|
||||||
|
matchedFallback: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#then keeps the user primary when it IS reachable (fast path preserved)", () => {
|
||||||
|
const result = resolveModelForDelegateTask({
|
||||||
|
userModel: "openai/gpt-5.5 xhigh",
|
||||||
|
userFallbackModels: ["openai/gpt-5.4"],
|
||||||
|
availableModels: new Set(["openai/gpt-5.5", "openai/gpt-5.4"]),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({ model: "openai/gpt-5.5", variant: "xhigh" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#then returns the user primary as-is when no user fallback is reachable either (trust-user legacy behavior)", () => {
|
||||||
|
const result = resolveModelForDelegateTask({
|
||||||
|
userModel: "opencode/gemini-3.1-pro high",
|
||||||
|
userFallbackModels: ["google/gemini-3.1-pro"],
|
||||||
|
availableModels: new Set(["openai/gpt-5.5"]),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({ model: "opencode/gemini-3.1-pro", variant: "high" })
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("#given provider cache exists and connected providers are known", () => {
|
describe("#given provider cache exists and connected providers are known", () => {
|
||||||
|
|||||||
@@ -57,10 +57,48 @@ export function resolveModelForDelegateTask(input: {
|
|||||||
const userModel = normalizeModel(input.userModel)
|
const userModel = normalizeModel(input.userModel)
|
||||||
if (userModel) {
|
if (userModel) {
|
||||||
const parsed = parseUserFallbackModel(userModel)
|
const parsed = parseUserFallbackModel(userModel)
|
||||||
if (parsed?.variant) {
|
const userResult = parsed?.variant
|
||||||
return { model: parsed.baseModel, variant: parsed.variant }
|
? { model: parsed.baseModel, variant: parsed.variant }
|
||||||
|
: { model: userModel }
|
||||||
|
|
||||||
|
// When the availability cache is warm AND the user provided fallback_models,
|
||||||
|
// verify the user's explicit primary model is actually reachable. If it is
|
||||||
|
// not but one of their configured fallback_models is, promote that fallback
|
||||||
|
// instead of returning an unreachable model. Cold cache (no availability
|
||||||
|
// data yet) preserves the legacy "trust the user" behavior.
|
||||||
|
const userFallbackModels = input.userFallbackModels
|
||||||
|
if (
|
||||||
|
input.availableModels.size > 0 &&
|
||||||
|
userFallbackModels &&
|
||||||
|
userFallbackModels.length > 0
|
||||||
|
) {
|
||||||
|
const providerHint = parsed?.providerHint
|
||||||
|
const primaryMatch = fuzzyMatchModel(userResult.model, input.availableModels, providerHint)
|
||||||
|
if (!primaryMatch) {
|
||||||
|
for (const fallbackModel of userFallbackModels) {
|
||||||
|
const parsedFallback = parseUserFallbackModel(fallbackModel)
|
||||||
|
if (!parsedFallback) continue
|
||||||
|
const fbMatch = fuzzyMatchModel(
|
||||||
|
parsedFallback.baseModel,
|
||||||
|
input.availableModels,
|
||||||
|
parsedFallback.providerHint,
|
||||||
|
)
|
||||||
|
if (fbMatch) {
|
||||||
|
log("[resolveModelForDelegateTask] user primary model unreachable; promoting user fallback_models entry", {
|
||||||
|
userPrimary: userResult.model,
|
||||||
|
selectedFallback: fbMatch,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
model: fbMatch,
|
||||||
|
variant: parsedFallback.variant,
|
||||||
|
matchedFallback: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { model: userModel }
|
|
||||||
|
return userResult
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectedProviders = input.availableModels.size === 0 ? connectedProvidersCache.readConnectedProvidersCache() : null
|
const connectedProviders = input.availableModels.size === 0 ? connectedProvidersCache.readConnectedProvidersCache() : null
|
||||||
|
|||||||
Reference in New Issue
Block a user