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:
+45
-25
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user