fix(delegate-task): honor user fallback_models when category primary is unreachable
resolveModelForDelegateTask returned input.userModel immediately without
availability checking when it was set, silently ignoring the user's
configured fallback_models. Effect: a team-mode category member
(e.g. hyperplan's 'artistry' category) configured with
{ model: "opencode/gemini-3.1-pro", fallback_models: [...] }
spawned with the unreachable primary even though a listed fallback was
reachable, leading to a dropped/broken team member instead of graceful
degradation.
Now, when the provider-models cache is warm AND userFallbackModels is
non-empty, the function verifies userModel is reachable via
fuzzyMatchModel. If not, it iterates userFallbackModels and promotes the
first reachable entry. Cold-cache (first-run) behavior is preserved -
the userModel is returned as-is when availability data is unavailable,
matching the existing 'trust the user' contract covered by the
pre-cache test fixtures.
Adds three regression tests covering: (1) unreachable primary + reachable
fallback -> fallback promoted, (2) reachable primary + reachable fallback
-> primary kept (fast path), (3) unreachable primary + unreachable
fallback -> legacy trust-user behavior preserved.
This commit is contained in:
@@ -57,10 +57,48 @@ export function resolveModelForDelegateTask(input: {
|
||||
const userModel = normalizeModel(input.userModel)
|
||||
if (userModel) {
|
||||
const parsed = parseUserFallbackModel(userModel)
|
||||
if (parsed?.variant) {
|
||||
return { model: parsed.baseModel, variant: parsed.variant }
|
||||
const userResult = 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 ? readConnectedProvidersCache() : null
|
||||
|
||||
Reference in New Issue
Block a user