ae0c106ed4
After the 4.2.0 unified-dispatch refactor (a42f894f/df198d8b/fee515c5/989ab717/dd3fecaf/1bbe065c/12bd6580), at least one caller in the new prompt-async-gate path forwards a FallbackModelObject (or some other non-string shape) into parsers that statically claim 'model: string'. The downstream .trim() call then throws 'model.trim is not a function', which rejects the session.processor promise and surfaces as 'Aborted process' + UI 'interrupted'. The issue (#4145) reports this aborts 90% of subagent dispatches across every provider on 4.2.0 + opencode 1.15.4. This patch adds a 'typeof x !== "string"' runtime guard at the four parser entrypoints called from the dispatch path: - src/shared/fallback-chain-from-models.ts :: parseVariantFromModel, parseFallbackModelEntry - src/tools/delegate-task/model-string-parser.ts :: parseVariantFromModelID, parseModelString - src/shared/model-string-parser.ts (duplicate file with same API) :: parseVariantFromModelID, parseModelString - src/features/claude-code-agent-loader/claude-model-mapper.ts :: mapClaudeModelString Each parser now returns undefined / { modelID: "" } for non-string input instead of throwing. This unblocks subagent dispatch and leaves the underlying caller bug for a follow-up. Regression coverage: three new tests in src/shared/fallback-chain-from-models.test.ts pin the non-string behavior (object, null/undefined, number). Existing 38 tests still pass. Total: 41/41 green, typecheck clean.
68 lines
1.8 KiB
TypeScript
68 lines
1.8 KiB
TypeScript
const KNOWN_VARIANTS = new Set([
|
|
"low",
|
|
"medium",
|
|
"high",
|
|
"xhigh",
|
|
"max",
|
|
"minimal",
|
|
"none",
|
|
"auto",
|
|
"thinking",
|
|
])
|
|
|
|
export function parseVariantFromModelID(rawModelID: string): { modelID: string; variant?: string } {
|
|
if (typeof rawModelID !== "string") {
|
|
return { modelID: "" }
|
|
}
|
|
const trimmedModelID = rawModelID.trim()
|
|
if (!trimmedModelID) {
|
|
return { modelID: "" }
|
|
}
|
|
|
|
const parenthesizedVariant = trimmedModelID.match(/^(.*)\(([^()]+)\)\s*$/)
|
|
if (parenthesizedVariant) {
|
|
const modelID = parenthesizedVariant[1]?.trim() ?? ""
|
|
const variant = parenthesizedVariant[2]?.trim()
|
|
return variant ? { modelID, variant } : { modelID }
|
|
}
|
|
|
|
const spaceVariant = trimmedModelID.match(/^(.*\S)\s+([a-z][a-z0-9_-]*)$/i)
|
|
if (spaceVariant) {
|
|
const modelID = spaceVariant[1]?.trim() ?? ""
|
|
const variant = spaceVariant[2]?.trim().toLowerCase()
|
|
if (variant && KNOWN_VARIANTS.has(variant)) {
|
|
return { modelID, variant }
|
|
}
|
|
}
|
|
|
|
return { modelID: trimmedModelID }
|
|
}
|
|
|
|
export function parseModelString(
|
|
model: string,
|
|
): { providerID: string; modelID: string; variant?: string } | undefined {
|
|
if (typeof model !== "string") return undefined
|
|
const trimmedModel = model.trim()
|
|
if (!trimmedModel) return undefined
|
|
|
|
const parts = trimmedModel.split("/")
|
|
if (parts.length < 2) {
|
|
return undefined
|
|
}
|
|
|
|
const providerID = parts[0]?.trim()
|
|
const rawModelID = parts.slice(1).join("/").trim()
|
|
if (!providerID || !rawModelID) {
|
|
return undefined
|
|
}
|
|
|
|
const parsedModel = parseVariantFromModelID(rawModelID)
|
|
if (!parsedModel.modelID) {
|
|
return undefined
|
|
}
|
|
|
|
return parsedModel.variant
|
|
? { providerID, modelID: parsedModel.modelID, variant: parsedModel.variant }
|
|
: { providerID, modelID: parsedModel.modelID }
|
|
}
|