Files
df18925e8d Repair OpenRouter image, video and audio integration
Preserve existing workflows while adding capability-based catalogs, dedicated
image requests, resumable native video, validated audio and explicit chat tiers.
Cover compatibility, billing-sensitive retries, credentials and cancellation.

Co-authored-by: Arthur Reboul Salze <22814805+ArthurReboulSalze@users.noreply.github.com>
Co-authored-by: Dookie <me@djdookie.de>
2026-10-01 22:22:39 -05:00

94 lines
5.6 KiB
JavaScript

// Pure workflow migrations, also exercised without a running ComfyUI frontend.
export const NODE_IDS = new Set(["OpenRouterNode", "openrouter_node"]);
export const WIDGET_NAMES = [
"api_key", "system_prompt", "user_message_box", "model", "web_search", "cheapest", "fastest",
"aspect_ratio", "image_resolution", "reasoning_effort", "seed", "control_after_generate",
"temperature", "pdf_engine", "chat_mode", "request_timeout", "request_type", "service_tier",
"image_quality", "image_background", "video_mode", "video_duration", "video_resolution",
"video_generate_audio", "video_wait_timeout", "video_job_id",
];
const HISTORICAL_NAMES = ["api_key", "system_prompt", "user_message_box", "model", "web_search",
"cheapest", "fastest", "temperature", "pdf_engine", "chat_mode"];
const COMMON_NAMES = HISTORICAL_NAMES.slice(0, 7);
const LEGACY_TAIL = ["temperature", "pdf_engine", "chat_mode"];
const DEFAULTS = ["", "You are a helpful assistant.", "Hello, how are you?", "openai/gpt-4o", false,
true, false, "auto", "auto", "auto", 0, "fixed", 1, "auto", false, 120, "chat", "auto", "auto",
"auto", "text_to_video", "auto", "auto", false, 900, ""];
export function migrateWorkflow(graph) {
for (const subgraph of graph.definitions?.subgraphs || []) migrateWorkflow(subgraph);
for (const node of graph.nodes || []) {
if (!NODE_IDS.has(node.type)) continue;
const oldOutputs = node.outputs || [];
const historical = oldOutputs.length === 3 && oldOutputs[1]?.name === "Stats";
if (historical) {
oldOutputs.splice(1, 0, {name: "image", type: "IMAGE", links: null});
for (const link of [...(graph.links || []), ...(graph.floatingLinks || [])]) {
if (Array.isArray(link) && link[1] === node.id && link[2] >= 1) link[2] += 1;
else if (!Array.isArray(link) && link.origin_id === node.id && link.origin_slot >= 1) link.origin_slot += 1;
}
}
if (oldOutputs.length === 4) oldOutputs.push({name: "video", type: "VIDEO", links: null});
oldOutputs.forEach((output, index) => { output.slot_index = index; });
let names = node.properties?.openrouter_widget_names;
if (!Array.isArray(names)) {
const old = node.widgets_values || [];
names = historical || typeof old[7] === "number" ? HISTORICAL_NAMES : WIDGET_NAMES;
// Actual released layouts: Sep 2025 image toggle, early Apr 2026
// image_size/seed, then aspect ratio, then reasoning and timeout.
if (!historical && typeof old[7] === "boolean") {
names = typeof old[8] === "string"
? [...COMMON_NAMES, "image_generation", "image_resolution", "seed", "control_after_generate", ...LEGACY_TAIL]
: [...COMMON_NAMES, "image_generation", ...LEGACY_TAIL];
} else if (!historical && typeof old[8] === "number") {
names = [...COMMON_NAMES, "image_resolution", "seed", "control_after_generate", ...LEGACY_TAIL];
} else if (!historical && typeof old[9] === "number") {
names = WIDGET_NAMES.filter(name => name !== "reasoning_effort");
}
}
const values = new Map(names.map((name, i) => [name, node.widgets_values?.[i]]));
for (const [name, value] of Object.entries(node.widgets_values_named || {})) {
if (WIDGET_NAMES.includes(name)) values.set(name, value);
}
node.widgets_values = WIDGET_NAMES.map((name, i) => values.get(name) ?? DEFAULTS[i]);
if (node.widgets_values_named) {
node.widgets_values_named = Object.fromEntries(WIDGET_NAMES.map((name, i) => [name, node.widgets_values[i]]));
}
node.properties ||= {};
node.properties.openrouter_widget_names = [...WIDGET_NAMES];
node.properties.openrouter_schema_version = 2;
}
return graph;
}
export function visibleInMode(name, mode, resume = false) {
if (["api_key", "request_type", "request_timeout"].includes(name)) return true;
if (mode === "video" && resume) return ["video_job_id", "video_wait_timeout"].includes(name);
if (name.startsWith("video_")) return mode === "video";
if (["service_tier", "system_prompt", "web_search", "cheapest", "fastest", "reasoning_effort",
"temperature", "pdf_engine", "chat_mode"].includes(name)) return mode === "chat";
if (["image_quality", "image_background"].includes(name)) return mode === "image";
if (name === "image_resolution") return mode !== "video";
return true;
}
export function modelOptions(catalog, mode, selected) {
const ids = (catalog[mode] || []).map(model => model.id).filter(Boolean).sort();
// Keep unavailable saved choices visible, so refresh never silently changes
// the model (or its price). Execution supplies the actionable validation.
if (selected && !ids.includes(selected)) ids.unshift(selected);
return ids;
}
export function scrubSerializedKey(node, serialized) {
// options.serialize controls the API prompt, not workflow persistence.
// In particular, the seed control must remain in this positional list.
const names = node.widgets?.filter(w => w.serialize !== false).map(w => w.name) || WIDGET_NAMES;
const index = names.indexOf("api_key");
if (Array.isArray(serialized.widgets_values) && index >= 0) serialized.widgets_values[index] = "";
if (serialized.widgets_values_named) serialized.widgets_values_named.api_key = "";
serialized.properties ||= {};
serialized.properties.openrouter_widget_names = names;
serialized.properties.openrouter_schema_version = 2;
}