Add LoRA stacks with per-item strengths and named video/image generation presets so setups survive reload without mixing Klein onto MiniMax. Keep LTX Pink Cherry and xAIGen LoRAs on xAIGen, and run Recommend on Beast.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 15:24:44 -05:00
co-authored by Cursor
parent 0af3cddb14
commit f8db0d626a
36 changed files with 1767 additions and 246 deletions
+45 -25
View File
@@ -17,23 +17,7 @@ export interface PromptJob {
const jobs = new Map<string, PromptJob>()
const MAX_JOBS = 20
function normalizeHost(raw: string, port = '') {
let host = String(raw || '').trim().replace(/\/$/, '')
if (!host) return ''
if (!/^https?:\/\//i.test(host)) host = `http://${host}`
try {
const url = new URL(host)
if (port && !url.port) url.port = port
return url.origin
} catch {
return port ? `${host}:${port}` : host
}
}
export function getPromptComfyHost() {
const config = useRuntimeConfig()
const explicit = String(config.promptComfyHost || process.env.PROMPT_COMFY_HOST || '').trim()
if (explicit) return normalizeHost(explicit)
return getSidecarImageHost()
}
@@ -44,14 +28,14 @@ export function promptComfyConfigured() {
async function promptComfyFetch(path: string, init?: RequestInit) {
const host = getPromptComfyHost()
if (!host) {
throw createError({ statusCode: 503, statusMessage: 'Sidecar Qwen VL is not configured. Set IMAGE_COMFY_HOST.' })
throw createError({ statusCode: 503, statusMessage: 'Qwen VL is not configured. Set COMFY_HOST or IMAGE_COMFY_HOST to Beast.' })
}
try {
return await fetch(`${host}${path}`, init)
} catch (error) {
throw createError({
statusCode: 502,
statusMessage: `Sidecar Qwen VL is unreachable (${host})`,
statusMessage: `Beast Qwen VL is unreachable (${host})`,
data: { cause: error instanceof Error ? error.message : String(error) }
})
}
@@ -75,7 +59,7 @@ async function uploadPromptImage(file: { filename: string; data: Buffer; type?:
body.append('type', 'input')
const res = await promptComfyFetch('/upload/image', { method: 'POST', body })
if (!res.ok) {
throw createError({ statusCode: 502, statusMessage: `Sidecar image upload failed (${res.status})` })
throw createError({ statusCode: 502, statusMessage: `Beast image upload failed (${res.status})` })
}
const uploaded = (await res.json()) as { name: string; subfolder?: string; type?: string }
return uploaded.name || filename
@@ -89,14 +73,48 @@ async function queuePromptGraph(graph: unknown, clientId: string) {
})
const payload = await res.json().catch(() => ({}))
if (!res.ok) {
const raw = JSON.stringify(payload)
if (/does not exist|class_type|not found/i.test(raw)) {
throw createError({
statusCode: 503,
statusMessage: 'Beast ComfyUI rejected the Qwen VL graph. The GGUF VLM nodes are missing on this instance — Recommend will not use the sidecar.',
data: payload
})
}
const message = (payload as { error?: { message?: string } }).error?.message
|| (payload as { node_errors?: unknown }).node_errors
|| `Sidecar queue failed (${res.status})`
|| `Beast queue failed (${res.status})`
throw createError({ statusCode: 502, statusMessage: String(message), data: payload })
}
return payload as { prompt_id: string }
}
const QWEN_VL_NODE_TYPES = ['GGUF_VLM_ModelLoader', 'GGUF_VLM_ImageToText', 'AigenPreviewText']
async function assertQwenVlWorkflow() {
const res = await promptComfyFetch('/object_info')
if (!res.ok) {
throw createError({
statusCode: 502,
statusMessage: `Beast ComfyUI did not return node info (${res.status})`
})
}
const info = await res.json().catch(() => null)
if (!info || typeof info !== 'object') {
throw createError({
statusCode: 502,
statusMessage: 'Beast ComfyUI returned unreadable node info'
})
}
const missing = QWEN_VL_NODE_TYPES.filter(name => !(name in (info as Record<string, unknown>)))
if (missing.length) {
throw createError({
statusCode: 503,
statusMessage: `Beast ComfyUI is missing Qwen VL nodes (${missing.join(', ')}). Recommend is wired to Beast only and will not fall back to the sidecar.`
})
}
}
async function fetchPromptHistory(promptId: string) {
const res = await promptComfyFetch(`/history/${encodeURIComponent(promptId)}`)
if (!res.ok) return null
@@ -149,9 +167,9 @@ function historyError(history: Record<string, unknown> | null, promptId: string)
const err = entry?.status?.messages?.find(([type]) => type === 'execution_error')?.[1]
const message = String(err?.exception_message || err?.message || '').trim()
if (status === 'interrupted') return 'Prompt recommend was interrupted.'
if (status === 'error') return message || 'Sidecar Qwen VL reported an execution error'
if (status === 'error') return message || 'Beast Qwen VL reported an execution error'
if (entry?.status?.completed && !extractPromptText(history, promptId)) {
return 'Sidecar Qwen VL finished without prompt text'
return 'Beast Qwen VL finished without prompt text'
}
return null
}
@@ -168,7 +186,7 @@ async function waitForPromptText(promptId: string, timeoutMs = 240_000) {
}
await new Promise(resolve => setTimeout(resolve, 1200))
}
throw createError({ statusCode: 504, statusMessage: 'Sidecar Qwen VL timed out while writing the prompt' })
throw createError({ statusCode: 504, statusMessage: 'Beast Qwen VL timed out while writing the prompt' })
}
export function createPromptJob(): PromptJob {
@@ -221,11 +239,13 @@ export async function runPromptRecommend(job: PromptJob, params: {
mode: string
}) {
job.status = 'running'
job.message = 'Checking Sidecar Qwen VL…'
job.message = 'Checking Beast Qwen VL…'
await ensureSidecarReady((status) => {
job.message = status.message
})
job.message = 'Uploading still to Sidecar Qwen VL…'
job.message = 'Confirming Qwen VL nodes on Beast…'
await assertQwenVlWorkflow()
job.message = 'Uploading still to Beast Qwen VL…'
const imageName = await uploadPromptImage(params.image, job.id)
job.message = 'Qwen VL is reading the still…'
const graph = buildVisionPromptWorkflow({