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
JavaScript
/**
* 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