Files
OmniRoute/open-sse/utils/comfyuiClient.ts
Diego Rodrigues de Sa e Souza ef777d77e1 feat: editable ComfyUI base-URL field + per-connection override for image/video/music generation (#6928) (#7232)
* feat(providers): editable ComfyUI base-URL field + per-connection override for image/video/music generation (#6928)

Adds a shared resolveComfyUiBaseUrl() helper (open-sse/utils/comfyuiClient.ts) that
prefers a per-connection providerSpecificData.baseUrl override over the registry
default, wired through the comfyui dispatch branch in the image, video, and music
generation handlers plus a best-effort authType:"none" credential lookup in all
three /v1/{images,videos,music}/generations routes (never hard-fails when no
connection exists, so zero-config localhost users are unaffected).

Surfaces an editable base-URL field on the ComfyUI connection form by adding it to
CONFIGURABLE_BASE_URL_PROVIDERS / DEFAULT_PROVIDER_BASE_URLS /
getProviderBaseUrlPlaceholder in providerPageHelpers.ts, so Docker-network setups
(e.g. http://comfyui:8188) can be configured the same way self-hosted chat
providers are.

Closes #6928

* refactor(6928): extract local-override credential lookup in media routes

The inline per-connection override block nested if>if inside the music and
videos POST handlers, taking each to cognitive complexity 16 (>15) — two NEW
violations that broke check:complexity-ratchets (892 > baseline 890).
Extracted resolveLocalOverrideCredentials() in both routes; behaviour is
unchanged. cognitive-complexity back to 890 = baseline.
2026-07-17 10:40:22 -03:00

150 lines
4.3 KiB
TypeScript

/**
* Shared ComfyUI API Client
*
* Used by image, video, and music handlers to submit workflows,
* poll for completion, and fetch output files from a ComfyUI server.
*/
type JsonRecord = Record<string, unknown>;
type ComfyOutputFile = {
filename: string;
subfolder?: string;
type?: string;
};
type ComfyNodeOutput = {
images?: ComfyOutputFile[];
gifs?: ComfyOutputFile[];
audio?: ComfyOutputFile[];
};
type ComfyHistoryEntry = {
outputs?: Record<string, ComfyNodeOutput>;
};
function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
/**
* Submit a workflow to ComfyUI for execution.
* @returns The prompt_id for polling
*/
export async function submitComfyWorkflow(baseUrl: string, workflow: object): Promise<string> {
const res = await fetch(`${baseUrl}/prompt`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: workflow }),
});
if (!res.ok) {
const errText = await res.text();
throw new Error(`ComfyUI submit failed (${res.status}): ${errText}`);
}
const data = toRecord(await res.json());
const promptId = data.prompt_id;
if (typeof promptId !== "string" || !promptId) {
throw new Error("ComfyUI submit failed: missing prompt_id");
}
return promptId;
}
/**
* Poll ComfyUI history endpoint until the prompt completes or times out.
* @returns The history entry for the completed prompt
*/
export async function pollComfyResult(
baseUrl: string,
promptId: string,
timeoutMs: number = 120_000
): Promise<ComfyHistoryEntry> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
await new Promise((r) => setTimeout(r, 2000));
const res = await fetch(`${baseUrl}/history/${promptId}`);
if (!res.ok) continue;
const data = toRecord(await res.json());
const entry = toRecord(data[promptId]) as ComfyHistoryEntry;
if (entry && entry.outputs && Object.keys(entry.outputs).length > 0) {
return entry;
}
}
throw new Error(`ComfyUI prompt ${promptId} timed out after ${timeoutMs}ms`);
}
/**
* Fetch an output file from ComfyUI.
* @returns The file contents as ArrayBuffer
*/
export async function fetchComfyOutput(
baseUrl: string,
filename: string,
subfolder: string,
type: string
): Promise<ArrayBuffer> {
const url = new URL(`${baseUrl}/view`);
url.searchParams.set("filename", filename);
url.searchParams.set("subfolder", subfolder);
url.searchParams.set("type", type);
const res = await fetch(url.toString());
if (!res.ok) {
throw new Error(`ComfyUI fetch output failed (${res.status})`);
}
return res.arrayBuffer();
}
/**
* Extract output files from a ComfyUI history entry.
* Returns an array of { filename, subfolder, type } for each output.
*/
export function extractComfyOutputFiles(
historyEntry: ComfyHistoryEntry
): Array<{ filename: string; subfolder: string; type: string }> {
const files: Array<{ filename: string; subfolder: string; type: string }> = [];
for (const nodeOutput of Object.values(historyEntry.outputs || {})) {
const outputs = nodeOutput.images || nodeOutput.gifs || nodeOutput.audio || [];
for (const file of outputs) {
files.push({
filename: file.filename,
subfolder: file.subfolder || "",
type: file.type || "output",
});
}
}
return files;
}
/**
* Resolve the ComfyUI base URL to use for a request.
*
* Prefers a per-connection override (`credentials.providerSpecificData.baseUrl`,
* the same storage convention self-hosted chat providers use — see
* `providerPageHelpers.ts`'s `CONFIGURABLE_BASE_URL_PROVIDERS`) over the registry
* default, so operators running ComfyUI on a Docker-network hostname (e.g.
* `http://comfyui:8188`) aren't stuck on `localhost:8188` (#6928). Falls back to
* `fallback` when no connection exists or no override is set — zero-config
* localhost users see no behavior change.
*/
export function resolveComfyUiBaseUrl(
credentials: { providerSpecificData?: { baseUrl?: unknown } | null } | null | undefined,
fallback: string
): string {
const psd = credentials?.providerSpecificData;
const override =
psd && typeof psd === "object" && typeof psd.baseUrl === "string" && psd.baseUrl.trim()
? psd.baseUrl.trim()
: null;
return override || fallback;
}