UNPKG

comfyui-mcp

Version:

Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),

1,149 lines • 45 kB
/** * comfy-kitchen: see what the kernel library can do on this GPU, find where the * open graph leaves it on the table, and apply the faster path with proof. * * Every recommendation is a conjunction of facts. A failed probe is `unknown`, * never a "no" — a recommendation fires only when every input it needs is known. */ import { execFile } from "node:child_process"; import { open, readdir, readFile } from "node:fs/promises"; import { join } from "node:path"; import { config, getComfyUIBaseUrl, isRemoteMode } from "../config.js"; import { getLogs, getSystemStats } from "../comfyui/client.js"; import { comfyuiFetch } from "../comfyui/fetch.js"; import { isApiFormat, isUiFormat } from "./workflow-converter.js"; import { resolveComfyuiPython } from "./workspace-env.js"; import { logger } from "../utils/logger.js"; export function known(value) { return { status: "known", value }; } export function unknown(reason) { return { status: "unknown", reason }; } export function isKnown(f) { return f.status === "known"; } export const KITCHEN_BACKENDS = ["hip", "cuda", "triton", "eager"]; /** * The python source the kitchen import-probe runs. Exported so a test can assert * the NEGATIVE that matters: we never `import torch` inside the probe budget. */ export const KITCHEN_PROBE_SOURCE = [ "import importlib.util as u, json, sys", "print('present', u.find_spec('comfy_kitchen') is not None)", "ver = ''", "try:", " import importlib.metadata as md", " ver = md.version('comfy-kitchen')", "except Exception:", " ver = ''", "print('version', ver)", "if u.find_spec('comfy_kitchen') is not None:", " try:", " import comfy_kitchen as ck", " try:", " print('backends', json.dumps(ck.list_backends(), default=str))", " except Exception as e:", " print('backends_error', type(e).__name__)", " try:", " print('int8', ck.int8_attention_is_available())", " except Exception:", " print('int8', 'unknown')", " except Exception as e:", " print('import_error', type(e).__name__)", ].join("\n"); const CK_ATTENTION_FLAG = "--use-ck-attention"; const SAGE_ATTENTION_FLAG = "--use-sage-attention"; const ENABLE_TRITON_FLAG = "--enable-triton-backend"; const DISABLE_TRITON_FLAG = "--disable-triton-backend"; const FP8_FAST = "fp8_e4m3fn_fast"; let hintEmittedThisProcess = false; /** Test hook: the proactive hint is once per process. */ export function resetKitchenHintSession() { hintEmittedThisProcess = false; } // --------------------------------------------------------------------------- // Log / argv / GPU parsers (pure) // --------------------------------------------------------------------------- /** Join /internal/logs payload shapes the same way env-capabilities does. */ export function logTextFromPayload(data) { const entries = data?.entries; if (Array.isArray(entries)) { return entries .map((e) => (typeof e === "string" ? e : (e?.m ?? ""))) .join("\n"); } if (typeof data === "string") return data; if (Array.isArray(data)) return data.map(String).join("\n"); try { return JSON.stringify(data); } catch { return String(data ?? ""); } } function matchingClose(open, close) { return (open === "{" && close === "}") || (open === "[" && close === "]") || (open === "(" && close === ")"); } function splitTopLevelDictionary(raw) { const text = raw.trim(); if (text.length < 2 || text[0] !== "{" || text[text.length - 1] !== "}") return undefined; const members = []; const stack = []; let quote; let escaped = false; let start = 1; for (let i = 1; i < text.length - 1; i++) { const ch = text[i]; if (quote) { if (escaped) escaped = false; else if (ch === "\\") escaped = true; else if (ch === quote) quote = undefined; continue; } if (ch === "'" || ch === '"') { quote = ch; } else if (ch === "{" || ch === "[" || ch === "(") { stack.push(ch); } else if (ch === "}" || ch === "]" || ch === ")") { const open = stack.pop(); if (!open || !matchingClose(open, ch)) return undefined; } else if (ch === "," && stack.length === 0) { const member = text.slice(start, i).trim(); if (!member) return undefined; members.push(member); start = i + 1; } } if (quote || stack.length > 0) return undefined; const last = text.slice(start, -1).trim(); if (last) members.push(last); return members; } function topLevelColon(member) { const stack = []; let quote; let escaped = false; for (let i = 0; i < member.length; i++) { const ch = member[i]; if (quote) { if (escaped) escaped = false; else if (ch === "\\") escaped = true; else if (ch === quote) quote = undefined; continue; } if (ch === "'" || ch === '"') { quote = ch; } else if (ch === "{" || ch === "[" || ch === "(") { stack.push(ch); } else if (ch === "}" || ch === "]" || ch === ")") { const open = stack.pop(); if (!open || !matchingClose(open, ch)) return -1; } else if (ch === ":" && stack.length === 0) { return i; } } return -1; } function parseKitchenDictionaryAvailable(raw) { const members = splitTopLevelDictionary(raw); if (!members) return undefined; let available; for (const member of members) { const colon = topLevelColon(member); if (colon < 0) return undefined; const key = member.slice(0, colon).trim(); const value = member.slice(colon + 1).trim(); if (!key || !value) return undefined; if (!/^(?:"available"|'available'|available)$/i.test(key)) continue; if (!/^true$/i.test(value) && !/^false$/i.test(value)) return undefined; if (available !== undefined) return undefined; available = /^true$/i.test(value); } return available; } export function parseKitchenLog(text) { const versionMatch = text.match(/comfy-kitchen version:\s*(\S+)/i); const backends = {}; const backendRe = /Found comfy_kitchen backend\s+(\w+)\s*:\s*([^\r\n]+)/gi; for (const m of text.matchAll(backendRe)) { const name = m[1].toLowerCase(); if (KITCHEN_BACKENDS.includes(name)) { const raw = m[2].trim(); const dictionaryAvailable = parseKitchenDictionaryAvailable(raw); // Older logs used "available (cached)" rather than a bare status. const available = dictionaryAvailable ?? /^(?:available|enabled|ok|loaded|true|ready)(?:\s+\(cached\))?$/i.test(raw); backends[name] = { available, raw }; } } const tritonEnable = /Found triton\s+(\S+)\.\s*Enabling comfy-kitchen triton backend/i.exec(text); const tritonFound = /Found triton\s+(\S+)/i.exec(text); const ckAttn = /Using Comfy Kitchen attention/i.test(text); const sage = /Using sage attention/i.test(text); return { version: versionMatch ? known(versionMatch[1]) : unknown("no comfy-kitchen version line in log"), backends, ckAttentionLog: ckAttn ? known(true) : unknown("log has no 'Using Comfy Kitchen attention' line (absence is not a no)"), tritonEnabledLog: /Enabling comfy-kitchen triton backend/i.test(text) ? known(true) : unknown("log has no kitchen triton-enable line"), tritonVersion: tritonEnable ? known(tritonEnable[1]) : tritonFound ? known(tritonFound[1]) : unknown("no 'Found triton' line"), sageLog: sage ? known(true) : unknown("log has no 'Using sage attention' line"), sageMissingLog: /No module named ['"]sageattention['"]|Could not load sageattention/i.test(text) ? known(true) : unknown("log does not report a sageattention import failure"), }; } export function parseKitchenArgv(argv) { if (!Array.isArray(argv)) { return { useCkAttention: false, useSageAttention: false, enableTritonBackend: false, disableTritonBackend: false, fastFp8MatrixMult: false, fastCublasOps: false, argvKnown: false, }; } const tokens = argv.filter((t) => typeof t === "string"); const has = (flag) => tokens.includes(flag); // `--fast fp8_matrix_mult` is a flag + value; also accept `--fast=fp8_matrix_mult` // and a bare `--fast` that turns every PerformanceFeature on. let fastFp8 = false; let fastCublas = false; let bareFast = false; for (let i = 0; i < tokens.length; i++) { const t = tokens[i]; if (t === "--fast") { const next = tokens[i + 1]; if (!next || next.startsWith("-")) { bareFast = true; } else if (next === "fp8_matrix_mult") fastFp8 = true; else if (next === "cublas_ops") fastCublas = true; } else if (t.startsWith("--fast=")) { const v = t.slice("--fast=".length); if (v === "fp8_matrix_mult") fastFp8 = true; if (v === "cublas_ops") fastCublas = true; } } return { useCkAttention: has(CK_ATTENTION_FLAG), useSageAttention: has(SAGE_ATTENTION_FLAG), enableTritonBackend: has(ENABLE_TRITON_FLAG), disableTritonBackend: has(DISABLE_TRITON_FLAG), fastFp8MatrixMult: bareFast || fastFp8, fastCublasOps: bareFast || fastCublas, argvKnown: true, }; } /** * Map a GPU marketing name to a compute capability. Unrecognized names stay * unknown — a filename or a close spelling is not an SM number. */ export function inferComputeFromGpu(gpuName, deviceType) { const type = (deviceType ?? "").toLowerCase(); let host = "unknown"; if (/^(hip|rocm)$/.test(type) || /radeon|instinct|\bamd\b/i.test(gpuName ?? "")) host = "rocm"; else if (/^cuda$/.test(type) || /nvidia|geforce|rtx|tesla|quadro/i.test(gpuName ?? "")) host = "cuda"; else if (/^mps$/.test(type) || /apple|metal/i.test(gpuName ?? "")) host = "mps"; else if (/^cpu$/.test(type)) host = "cpu"; const n = (gpuName ?? "").replace(/^(?:cuda|cpu|mps|xpu|hip|rocm):\d+\s+/i, ""); let sm; // Blackwell consumer is sm_120; datacenter B100/B200 is sm_100. Both are ≥ 10.0. if (/\b(?:RTX\s*50\d{2}|GeForce\s*RTX\s*50\d{2})\b/i.test(n) || /Blackwell/i.test(n)) sm = 12.0; else if (/\b(?:B100|B200|GB200)\b/i.test(n)) sm = 10.0; else if (/\b(?:RTX\s*40\d{2}|GeForce\s*RTX\s*40\d{2}|L40S?\b|L4\b|RTX\s*6000\s*Ada|Ada)\b/i.test(n)) sm = 8.9; else if (/\b(?:H100|H200|GH200)\b/i.test(n)) sm = 9.0; else if (/\b(?:RTX\s*30\d{2}|GeForce\s*RTX\s*30\d{2}|A6000|A40)\b/i.test(n)) sm = 8.6; else if (/\bA100\b/i.test(n)) sm = 8.0; else if (/\b(?:RTX\s*20\d{2}|GeForce\s*RTX\s*20\d{2}|T4\b)\b/i.test(n)) sm = 7.5; if (sm === undefined) { const reason = gpuName ? `GPU name "${gpuName}" is not in the known SM table` : "no GPU name to map to a compute capability"; return { sm: unknown(reason), fp8: unknown(reason), nvfp4: unknown(reason), mxfp8: unknown(reason), host, }; } return { sm: known(sm), fp8: known(sm >= 8.9), nvfp4: known(sm >= 10.0), mxfp8: known(sm >= 10.0), host, }; } export function parseKitchenProbe(stdout) { const out = stdout || ""; if (!out.trim()) { return { present: unknown("kitchen import-probe produced no output"), version: unknown("kitchen import-probe produced no output"), backends: {}, int8: unknown("kitchen import-probe produced no output"), }; } const presentLine = out.match(/^present\s+(True|False)/m); const present = presentLine ? known(presentLine[1] === "True") : unknown("probe did not print a present line"); const ver = out.match(/^version\s+(\S+)/m)?.[1]; const backends = {}; const backendsJson = out.match(/^backends\s+(.+)$/m)?.[1]; if (backendsJson && backendsJson !== "") { try { const parsed = JSON.parse(backendsJson); for (const name of KITCHEN_BACKENDS) { const entry = parsed[name]; if (entry && typeof entry === "object") { const rec = entry; if (typeof rec.available === "boolean") backends[name] = { available: rec.available }; } else if (typeof entry === "boolean") { backends[name] = { available: entry }; } } } catch { /* leave backends empty — unknown, not a no */ } } const int8Line = out.match(/^int8\s+(True|False|unknown|True|False)/m); let int8 = unknown("probe did not print int8"); if (int8Line) { if (int8Line[1] === "True") int8 = known(true); else if (int8Line[1] === "False") int8 = known(false); else int8 = unknown("int8_attention_is_available raised"); } return { present, version: ver && ver.length > 0 ? known(ver) : unknown("probe printed no comfy-kitchen version"), backends, int8, }; } export function parseCliArgsFlagSupport(source) { if (!source) { return { ckAttention: unknown("comfy/cli_args.py was not readable"), tritonBackend: unknown("comfy/cli_args.py was not readable"), }; } return { ckAttention: known(/use-ck-attention|use_ck_attention/.test(source)), tritonBackend: known(/enable-triton-backend|enable_triton_backend/.test(source)), }; } export function packageVersionFromStats(stats, name) { const pkgs = stats?.system?.comfy_package_versions; if (!Array.isArray(pkgs)) return unknown("system_stats has no comfy_package_versions"); const hit = pkgs.find((p) => p && typeof p === "object" && p.name === name); const installed = hit?.installed; if (typeof installed === "string" && installed.trim()) return known(installed.trim()); return unknown(`${name} is not in comfy_package_versions`); } export function quantFromSafetensorsHeader(header) { const meta = header.__metadata__; const blobs = [header._quantization_metadata, header]; if (meta && typeof meta === "object") blobs.unshift(meta); const asText = (v) => { if (typeof v === "string") return v; try { return JSON.stringify(v) ?? ""; } catch { return ""; } }; for (const blob of blobs) { const s = asText(blob).toLowerCase(); if (!s) continue; if (s.includes("nvfp4")) return known("nvfp4"); if (s.includes("mxfp8")) return known("mxfp8"); if (s.includes("fp8_e5m2") || s.includes("e5m2")) return known("fp8_e5m2"); if (s.includes("fp8_e4m3fn") || s.includes("e4m3") || s.includes("fp8")) return known("fp8_e4m3fn"); } // A successfully-read header with no quant metadata is an unquantized file. return known("none"); } /** Sibling NVFP4 file of the same stem, if one is listed. Filename only — not a guess. */ export function findNvfp4Variant(filename, localNames) { if (!filename || localNames.length === 0) return undefined; const base = filename.replace(/\.(safetensors|sft|gguf)$/i, ""); const stem = base.replace(/[-_.]?(fp8|bf16|fp16|nvfp4|mxfp8)[^/]*$/i, ""); const hit = localNames.find((n) => { if (n === filename) return false; const lower = n.toLowerCase(); if (!lower.includes("nvfp4")) return false; const nBase = n.replace(/\.(safetensors|sft|gguf)$/i, ""); return nBase.toLowerCase().startsWith(stem.toLowerCase()) || n.toLowerCase().includes(stem.toLowerCase()); }); return hit; } export function tritonAtLeast(version, minMinor) { const m = version.trim().match(/^(\d+)\.(\d+)/); if (!m) return false; const major = Number(m[1]); const minor = Number(m[2]); if (major > 3) return true; if (major < 3) return false; return minor >= minMinor; } // --------------------------------------------------------------------------- // Graph walking // --------------------------------------------------------------------------- function asRecord(v) { if (v && typeof v === "object" && !Array.isArray(v)) return v; return undefined; } function namedWidgets(node) { const captured = asRecord(node.capturedWidgetValues) ?? asRecord(node.resolvedWidgetValues); if (captured) return captured; const widgets = node.widgets; if (widgets && typeof widgets === "object" && !Array.isArray(widgets)) { return widgets; } if (Array.isArray(widgets)) { const out = {}; for (const w of widgets) { const rec = asRecord(w); if (rec && typeof rec.name === "string") out[rec.name] = rec.value; } return out; } return {}; } function isUnetLoaderType(type) { return type === "UNETLoader" || /UNETLoader$/i.test(type); } /** * Loaders we can name-read. Positional widgets_values are used ONLY for the * stock UNETLoader (unet_name, weight_dtype) — any other packing is unknown. */ export function extractLoaders(graph, quantOf) { const out = []; const push = (node_id, node_type, unet_name, weight_dtype) => { const quant = isKnown(unet_name) && quantOf ? quantOf(unet_name.value) : unknown(isKnown(unet_name) ? "model.quant was not probed (remote, or no local path)" : "loader filename unknown, so quant is unknown"); out.push({ node_id, node_type, unet_name, weight_dtype, quant }); }; if (isApiFormat(graph)) { for (const [id, node] of Object.entries(graph)) { const type = node.class_type; if (!isUnetLoaderType(type)) continue; const inputs = node.inputs ?? {}; const name = typeof inputs.unet_name === "string" ? known(inputs.unet_name) : unknown("UNETLoader.unet_name is not a string in this graph"); const dtype = typeof inputs.weight_dtype === "string" ? known(inputs.weight_dtype) : unknown("UNETLoader.weight_dtype is not a string in this graph"); push(String(id), type, name, dtype); } return out; } if (isUiFormat(graph)) { for (const node of graph.nodes ?? []) { const type = node.type; if (!isUnetLoaderType(type)) continue; const rec = node; const named = namedWidgets(rec); let name = typeof named.unet_name === "string" ? known(named.unet_name) : unknown("UNETLoader.unet_name is not a named widget on this node"); let dtype = typeof named.weight_dtype === "string" ? known(named.weight_dtype) : unknown("UNETLoader.weight_dtype is not a named widget on this node"); // Stock UNETLoader widgets_values is [unet_name, weight_dtype]. if ((!isKnown(name) || !isKnown(dtype)) && type === "UNETLoader") { const wv = node.widgets_values; if (Array.isArray(wv) && wv.length === 2 && typeof wv[0] === "string" && typeof wv[1] === "string") { if (!isKnown(name)) name = known(wv[0]); if (!isKnown(dtype)) dtype = known(wv[1]); } } push(String(node.id), type, name, dtype); } return out; } // graph_query detail rows, or a { nodes: [...] } live-state document. const rows = Array.isArray(graph?.nodes) ? graph.nodes : Array.isArray(graph) ? graph : []; for (const row of rows) { const rec = asRecord(row); if (!rec) continue; const type = typeof rec.type === "string" ? rec.type : typeof rec.class_type === "string" ? rec.class_type : ""; if (!isUnetLoaderType(type)) continue; const id = rec.id ?? rec.node_id; const named = namedWidgets(rec); const name = typeof named.unet_name === "string" ? known(named.unet_name) : unknown("loader filename is not on this node summary"); const dtype = typeof named.weight_dtype === "string" ? known(named.weight_dtype) : unknown("weight_dtype is not on this node summary"); push(String(id), type, name, dtype); } return out; } // --------------------------------------------------------------------------- // Assess — recommendations fire only when every input is known // --------------------------------------------------------------------------- export function assessKitchen(status, graph, opts = {}) { const recs = []; const loaders = extractLoaders(graph, opts.quantOf); if (isKnown(status.present) && status.present.value === true && isKnown(status.gpu.fp8) && status.gpu.fp8.value) { for (const loader of loaders) { if (!isKnown(loader.weight_dtype) || !isKnown(loader.quant)) continue; if (loader.weight_dtype.value !== "default") continue; if (loader.quant.value !== "none") continue; recs.push({ id: `fp8_unet_fast:${loader.node_id}`, kind: "fp8_unet_fast", node_id: loader.node_id, node_type: loader.node_type, why: `UNETLoader ${loader.node_id} is on weight_dtype=default with an unquantized (bf16) file, this GPU has fp8 compute, and kitchen is present.`, safe: "Reversible widget edit. No restart. fp8_e4m3fn_fast turns on ComfyUI's fp8_optimizations.", restart: false, download: false, change: { type: "widget", widget: "weight_dtype", value: FP8_FAST, previous: loader.weight_dtype.value, }, }); } } const sageAbsent = status.flags.argvKnown && !status.flags.useSageAttention && isKnown(status.sageattentionInstalled) && status.sageattentionInstalled.value === false; if (sageAbsent && isKnown(status.present) && status.present.value === true && isKnown(status.gpu.int8Attention) && status.gpu.int8Attention.value === true && status.flags.argvKnown && !status.flags.useCkAttention) { recs.push({ id: "ck_attention", kind: "ck_attention", why: "sageattention is not installed, --use-sage-attention is absent, kitchen is present, and INT8 attention is available on this GPU.", safe: "Kitchen's --use-ck-attention does not need a sageattention wheel. Restart required; consent-gated.", restart: true, download: false, change: { type: "flag", flag: CK_ATTENTION_FLAG }, }); } if (isKnown(status.gpu.nvfp4) && status.gpu.nvfp4.value === true && opts.localFilenames) { for (const loader of loaders) { if (!isKnown(loader.unet_name) || !isKnown(loader.quant)) continue; if (loader.quant.value !== "none" && loader.quant.value !== "fp8_e4m3fn" && loader.quant.value !== "fp8_e5m2") continue; const variant = findNvfp4Variant(loader.unet_name.value, opts.localFilenames); if (!variant) continue; recs.push({ id: `nvfp4_swap:${loader.node_id}`, kind: "nvfp4_swap", node_id: loader.node_id, node_type: loader.node_type, why: `Blackwell GPU and an NVFP4 sibling of ${loader.unet_name.value} is already on disk (${variant}).`, safe: "Widget swap to the local NVFP4 file. Download is not required because the variant is already listed.", restart: false, download: false, change: { type: "model_swap", widget: "unet_name", value: variant, previous: loader.unet_name.value, filename: loader.unet_name.value, variant, }, }); } } if (status.host === "rocm" && isKnown(status.present) && status.present.value === true && isKnown(status.tritonVersion) && tritonAtLeast(status.tritonVersion.value, 7) && status.flags.argvKnown && !status.flags.enableTritonBackend && !status.flags.disableTritonBackend) { recs.push({ id: "triton_backend", kind: "triton_backend", why: `ROCm host, kitchen present, triton ${status.tritonVersion.value} (≥ 3.7), and --enable-triton-backend is not on argv.`, safe: "ComfyUI flag. Restart required; consent-gated. On master this is a ComfyUI flag, not a SwarmUI-only flag.", restart: true, download: false, change: { type: "flag", flag: ENABLE_TRITON_FLAG }, }); } return recs; } /** * One sentence naming the gap and the tool. Empty when the conjunction is not * certain, or when this process has already hinted. */ export function kitchenProactiveHint(status, recs) { if (hintEmittedThisProcess) return undefined; const fire = recs.filter((r) => r.kind === "fp8_unet_fast" || r.kind === "ck_attention"); if (fire.length === 0) return undefined; if (!(isKnown(status.present) && status.present.value)) return undefined; hintEmittedThisProcess = true; const names = fire.map((r) => r.kind).join(", "); return `Kitchen is present and ${names} is certain on this machine — call kitchen (action:"assess") or panel_kitchen to see the faster path.`; } export function formatKitchenEnvSnippet(status) { if (!isKnown(status.present)) return undefined; if (!status.present.value) return "Kitchen: not installed"; const ver = isKnown(status.version) ? status.version.value : "?"; const loaded = KITCHEN_BACKENDS.filter((b) => { const s = status.backends[b].available; return isKnown(s) && s.value; }); const backendBit = loaded.length ? ` (${loaded.join(",")})` : ""; return `Kitchen: ${ver}${backendBit}`; } // --------------------------------------------------------------------------- // Merge status from the independent sources // --------------------------------------------------------------------------- function emptyBackends() { const out = {}; for (const name of KITCHEN_BACKENDS) { out[name] = { name, available: unknown("no log line and no import-probe for this backend") }; } return out; } export function mergeKitchenStatus(parts) { const log = parts.log; const probe = parts.probe; const compute = inferComputeFromGpu(parts.gpuName, parts.gpuType); let present = unknown("no log version, no comfy_package_versions, no import-probe"); if (log && isKnown(log.version)) present = known(true); else if (parts.pkgVersion && isKnown(parts.pkgVersion)) present = known(true); else if (probe && isKnown(probe.present)) present = probe.present; let version = unknown("kitchen version not in log, packages, or probe"); if (log && isKnown(log.version)) version = log.version; else if (parts.pkgVersion && isKnown(parts.pkgVersion)) version = parts.pkgVersion; else if (probe && isKnown(probe.version)) version = probe.version; const backends = emptyBackends(); for (const name of KITCHEN_BACKENDS) { const fromLog = log?.backends[name]; const fromProbe = probe?.backends[name]; if (fromLog) backends[name] = { name, available: known(fromLog.available) }; else if (fromProbe) backends[name] = { name, available: known(fromProbe.available) }; } const ckFlag = parts.flags.argvKnown ? known(parts.flags.useCkAttention) : unknown("argv was not reported"); const sageFlag = parts.flags.argvKnown ? known(parts.flags.useSageAttention) : unknown("argv was not reported"); const ckLog = log?.ckAttentionLog ?? unknown("log was not read"); let ck = unknown("need both argv --use-ck-attention and the log line"); if (isKnown(ckFlag) && !ckFlag.value) ck = known(false); else if (isKnown(ckFlag) && ckFlag.value && isKnown(ckLog) && ckLog.value) ck = known(true); let sageInstalled = parts.sageInstalled ?? unknown("sageattention was not probed"); if (log && isKnown(log.sageLog) && log.sageLog.value) sageInstalled = known(true); else if (log && isKnown(log.sageMissingLog) && log.sageMissingLog.value) sageInstalled = known(false); let int8 = probe?.int8 ?? unknown("int8_attention_is_available was not probed"); // A server that is up with --use-ck-attention has already imported kitchen attention. if (parts.flags.argvKnown && parts.flags.useCkAttention) int8 = known(true); let flagSupport = parts.flagSupport ?? { ckAttention: unknown("ComfyUI-side flag support was not probed"), tritonBackend: unknown("ComfyUI-side flag support was not probed"), }; if (parts.flags.argvKnown && parts.flags.useCkAttention) flagSupport = { ...flagSupport, ckAttention: known(true) }; if (parts.flags.argvKnown && (parts.flags.enableTritonBackend || parts.flags.disableTritonBackend)) { flagSupport = { ...flagSupport, tritonBackend: known(true) }; } let tritonVersion = log?.tritonVersion ?? unknown("triton version not in log"); if (log && isKnown(log.tritonEnabledLog) && log.tritonEnabledLog.value && !isKnown(tritonVersion)) { tritonVersion = unknown("triton backend enabled in log but version token missing"); } return { location: parts.location, present, version, backends, attention: { ck, ckFlag, ckLog, sageFlag }, gpu: { name: parts.gpuName, type: parts.gpuType, sm: compute.sm, fp8: compute.fp8, nvfp4: compute.nvfp4, mxfp8: compute.mxfp8, int8Attention: int8, }, flags: parts.flags, flagSupport, sageattentionInstalled: sageInstalled, tritonVersion, host: compute.host, sources: { log: !!log, stats: parts.flags.argvKnown || !!parts.gpuName, probe: !!probe, cliArgs: !!parts.flagSupport && isKnown(parts.flagSupport.ckAttention), }, }; } // --------------------------------------------------------------------------- // Apply // --------------------------------------------------------------------------- export function compareProof(before, after) { if (after.outputBlack) return { ok: false, reason: "after-run output is black" }; if (after.outputNan) return { ok: false, reason: "after-run output is NaN" }; if (typeof before.secondsPerIteration === "number" && typeof after.secondsPerIteration === "number" && after.secondsPerIteration > before.secondsPerIteration) { return { ok: false, reason: `after-run is slower (s/it ${before.secondsPerIteration} → ${after.secondsPerIteration})`, }; } return { ok: true }; } export function formatProofSummary(before, after, rec) { const sit = typeof before.secondsPerIteration === "number" && typeof after.secondsPerIteration === "number" ? `s/it ${before.secondsPerIteration} → ${after.secondsPerIteration}` : "s/it unknown"; const vram = typeof before.peakVramGb === "number" && typeof after.peakVramGb === "number" ? `peak VRAM ${before.peakVramGb} → ${after.peakVramGb} GB` : "peak VRAM unknown"; const widgets = rec.change.type === "widget" || rec.change.type === "model_swap" ? 1 : 0; const flags = rec.change.type === "flag" ? 1 : 0; const color = after.outputBlack ? "output black" : after.outputNan ? "output NaN" : "output non-black"; return `${sit}, ${vram}, ${color}, ${widgets} widget changed, ${flags} flags changed`; } export async function applyKitchenRecommendation(opts) { const { rec } = opts; if ((rec.restart || rec.download) && !opts.confirm) { return { applied: false, needs_confirm: true, recommendation: rec, proof: { status: "not_run" }, }; } if (rec.change.type === "flag") { const flag = rec.change.flag; if (!flag) { return { applied: false, recommendation: rec, proof: { status: "failed" }, flag_note: "This flag recommendation is malformed: it has no launch flag.", }; } if (opts.applyFlag) { try { const outcome = await opts.applyFlag(flag); return { applied: outcome.applied, recommendation: rec, proof: { status: "not_run" }, flag_note: outcome.note, restart: outcome.restart, }; } catch (err) { return { applied: false, recommendation: rec, proof: { status: "failed" }, flag_note: `Could not apply ${flag}: ${err instanceof Error ? err.message : String(err)} ` + "The launch outcome is not confirmed; verify ComfyUI's current launch arguments before retrying.", }; } } // restart_comfyui replays the previous argv; without a bound flag mutator this // process does not inject flags and must not claim that it did. return { applied: false, needs_confirm: false, recommendation: rec, proof: { status: "not_run" }, flag_note: `Add ${flag} to the ComfyUI launch line, then restart_comfyui / panel_restart_comfyui. ` + `Those tools replay the previous argv and do not compose a new one — applying the flag here would claim a restart that did not change argv.`, }; } const widget = rec.change.widget; const value = rec.change.value; const nodeId = rec.node_id; if (!widget || value === undefined || !nodeId) { return { applied: false, recommendation: rec, proof: { status: "failed" }, }; } if (!opts.applyWidget) { return { applied: false, recommendation: rec, proof: { status: "not_run" }, flag_note: "No widget mutator is bound (use panel_kitchen apply on the live graph, or pass a workflow and apply locally).", }; } let before; if (opts.runProof) before = await opts.runProof("before"); const previous = await opts.applyWidget(nodeId, widget, value); if (!opts.runProof) { return { applied: true, recommendation: rec, previous, proof: { status: "not_run", summary: "change applied; proof requires a before/after run on the same seed" }, }; } const after = await opts.runProof("after"); const verdict = before ? compareProof(before, after) : { ok: !after.outputBlack && !after.outputNan }; if (!verdict.ok) { if (opts.revertWidget && rec.change.previous !== undefined) { await opts.revertWidget(nodeId, widget, rec.change.previous); } return { applied: false, reverted: true, revert_reason: verdict.reason, recommendation: rec, previous, proof: { status: "reverted", before, after, summary: before ? formatProofSummary(before, after, rec) : verdict.reason, }, }; } return { applied: true, recommendation: rec, previous, proof: { status: "proven", before, after, summary: before ? formatProofSummary(before, after, rec) : "after-run output non-black", }, }; } function locationOf() { return isRemoteMode() ? "REMOTE" : "LOCAL"; } async function defaultFetchLog() { try { const lines = await getLogs({ maxLines: 2000 }); return lines.join("\n"); } catch (err) { logger.info("kitchen: /internal/logs unreadable", { error: err instanceof Error ? err.message : String(err), }); // Fall back to the same endpoint env-capabilities uses, so a client-library // failure is not the only path. try { const res = await comfyuiFetch(`${getComfyUIBaseUrl().replace(/\/+$/, "")}/internal/logs`, { signal: AbortSignal.timeout(4000), }); if (!res.ok) return ""; return logTextFromPayload(await res.json()); } catch { return ""; } } } async function defaultFetchStats() { try { return await getSystemStats(); } catch (err) { logger.info("kitchen: /system_stats unreadable", { error: err instanceof Error ? err.message : String(err), }); return undefined; } } function runKitchenProbe(pythonExe, timeoutMs) { return new Promise((resolve) => { let done = false; const child = execFile(pythonExe, ["-u", "-c", KITCHEN_PROBE_SOURCE], { timeout: timeoutMs, windowsHide: true }, (_err, stdout) => { if (done) return; done = true; resolve((stdout || "").toString()); }); child.on?.("error", () => { if (done) return; done = true; resolve(""); }); }); } const SAFETENSORS_HEADER_MAX = 64 * 1024 * 1024; async function readSafetensorsHeaderObject(filePath) { let fh; try { fh = await open(filePath, "r"); const lenBuf = Buffer.alloc(8); if ((await fh.read(lenBuf, 0, 8, 0)).bytesRead < 8) return undefined; const headerLen = Number(lenBuf.readBigUInt64LE(0)); if (!Number.isSafeInteger(headerLen) || headerLen <= 0 || headerLen > SAFETENSORS_HEADER_MAX) { return undefined; } const headerBuf = Buffer.alloc(headerLen); if ((await fh.read(headerBuf, 0, headerLen, 8)).bytesRead < headerLen) return undefined; const header = JSON.parse(headerBuf.toString("utf8")); if (!header || typeof header !== "object" || Array.isArray(header)) return undefined; return header; } catch { return undefined; } finally { await fh?.close().catch(() => undefined); } } async function findLocalModelFile(filename, comfyuiPath) { const dirs = ["diffusion_models", "unet", "checkpoints", "text_encoders", "vae"]; for (const dir of dirs) { const p = join(comfyuiPath, "models", dir, filename); try { const header = await readSafetensorsHeaderObject(p); if (header) return p; } catch { /* try next */ } } return undefined; } export async function readQuantForFilename(filename, opts = {}) { if ((opts.location ?? locationOf()) === "REMOTE") { return unknown("model.quant is unknown on a remote ComfyUI (the file is not on this machine)"); } const root = opts.comfyuiPath ?? config.comfyuiPath; if (!root) return unknown("model.quant is unknown without COMFYUI_PATH"); const filePath = await findLocalModelFile(filename, root); if (!filePath) return unknown(`no local safetensors file named ${filename} under models/`); const header = await readSafetensorsHeaderObject(filePath); if (!header) return unknown(`could not read safetensors header of ${filename}`); return quantFromSafetensorsHeader(header); } export async function listLocalUnetFilenames(opts = {}) { if ((opts.location ?? locationOf()) === "REMOTE") return undefined; const root = opts.comfyuiPath ?? config.comfyuiPath; if (!root) return undefined; const names = []; for (const dir of ["diffusion_models", "unet", "checkpoints"]) { try { const entries = await readdir(join(root, "models", dir)); for (const e of entries) names.push(e); } catch { /* missing dir is not a no */ } } return names; } export async function assessKitchenGraph(status, graph, opts = {}) { const loaders = extractLoaders(graph); const quantMap = new Map(); for (const loader of loaders) { if (!isKnown(loader.unet_name)) continue; if (!quantMap.has(loader.unet_name.value)) { quantMap.set(loader.unet_name.value, await readQuantForFilename(loader.unet_name.value, opts)); } } const localFilenames = await listLocalUnetFilenames(opts); return assessKitchen(status, graph, { localFilenames, quantOf: (filename) => quantMap.get(filename) ?? unknown("model.quant was not probed"), }); } export async function gatherKitchenStatus(opts = {}) { const location = opts.location ?? locationOf(); const stats = (opts.stats ?? (await (opts.fetchStats ?? defaultFetchStats)())); const logText = opts.logText ?? (await (opts.fetchLog ?? defaultFetchLog)()); const log = logText.trim() ? parseKitchenLog(logText) : undefined; const argv = stats?.system?.argv; const flags = parseKitchenArgv(argv); const gpu = stats?.devices?.find((d) => (d.type ?? "").toLowerCase() !== "cpu") ?? stats?.devices?.[0]; const pkgVersion = packageVersionFromStats(stats, "comfy-kitchen"); let probe; const remote = location === "REMOTE"; if (!remote) { const python = opts.pythonExe ?? resolveComfyuiPython(opts.comfyuiPath ?? config.comfyuiPath, argv).python; const stdout = opts.probeStdout !== undefined ? opts.probeStdout : python ? await (opts.runProbe ?? ((p) => runKitchenProbe(p, opts.probeTimeoutMs ?? 5000)))(python) : ""; if (stdout) probe = parseKitchenProbe(stdout); } let flagSupport; const comfyuiPath = opts.comfyuiPath ?? config.comfyuiPath; if (opts.cliArgsSource !== undefined) { flagSupport = parseCliArgsFlagSupport(opts.cliArgsSource); } else if (!remote && comfyuiPath) { const reader = opts.readCliArgs ?? (async (p) => { try { return await readFile(join(p, "comfy", "cli_args.py"), "utf8"); } catch { return undefined; } }); flagSupport = parseCliArgsFlagSupport(await reader(comfyuiPath)); } return mergeKitchenStatus({ location, log, flags, gpuName: gpu?.name, gpuType: gpu?.type, pkgVersion, probe, flagSupport, sageInstalled: opts.sageInstalled, }); } //# sourceMappingURL=kitchen.js.map