@openclaw/comfy-provider 2026.9.1-beta.1 → 2026.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,14 @@
1
+ import { randomInt } from "node:crypto";
1
2
  import fs from "node:fs/promises";
3
+ import { bufferToBlobPart } from "openclaw/plugin-sdk/blob-runtime";
2
4
  import { resolveGeneratedMediaMaxBytes } from "openclaw/plugin-sdk/media-generation-runtime";
3
5
  import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
4
6
  import { resolvePositiveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
5
7
  import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
6
8
  import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
7
- import { assertOkOrThrowHttpError, normalizeBaseUrl, readProviderJsonResponse, resolveProviderHttpRequestConfig } from "openclaw/plugin-sdk/provider-http";
9
+ import { assertOkOrThrowHttpError, normalizeBaseUrl, readProviderJsonResponse, redactProviderResponseErrorText, resolveProviderHttpRequestConfig } from "openclaw/plugin-sdk/provider-http";
8
10
  import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
9
- import { normalizeSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input-runtime";
11
+ import { normalizeSecretInputString, resolveConfiguredSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input-runtime";
10
12
  import { canResolveEnvSecretRefInReadOnlyPath } from "openclaw/plugin-sdk/secret-ref-readonly";
11
13
  import { fetchWithSsrFGuard, isPrivateOrLoopbackHost, mergeSsrFPolicies, ssrfPolicyFromHttpBaseUrlAllowedOrigin } from "openclaw/plugin-sdk/ssrf-runtime";
12
14
  import { asBoolean, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -16,8 +18,10 @@ const DEFAULT_COMFY_LOCAL_BASE_URL = "http://127.0.0.1:8188";
16
18
  const DEFAULT_COMFY_CLOUD_BASE_URL = "https://cloud.comfy.org";
17
19
  const DEFAULT_PROMPT_INPUT_NAME = "text";
18
20
  const DEFAULT_INPUT_IMAGE_INPUT_NAME = "image";
21
+ const DEFAULT_SEED_INPUT_NAME = "seed";
19
22
  const DEFAULT_POLL_INTERVAL_MS = 1500;
20
- const DEFAULT_TIMEOUT_MS = 5 * 6e4;
23
+ const DEFAULT_TIMEOUT_MS = 3e5;
24
+ const RANDOM_SEED_EXCLUSIVE_MAX = 2 ** 48 - 1;
21
25
  const DEFAULT_COMFY_MODEL = "workflow";
22
26
  function readConfigInteger(config, key) {
23
27
  const value = config[key];
@@ -107,6 +111,34 @@ function setWorkflowInput(params) {
107
111
  if (!isRecord(inputs)) throw new Error(`Comfy workflow node "${params.nodeId}" is missing an inputs object`);
108
112
  inputs[params.inputName] = params.value;
109
113
  }
114
+ async function resolveComfyHeadersConfig(value, cfg) {
115
+ const headers = new Headers();
116
+ if (!isRecord(value)) return headers;
117
+ for (const [name, headerValue] of Object.entries(value)) {
118
+ const path = `plugins.entries.comfy.config.headers.${name}`;
119
+ const inspected = resolveSecretInputString({
120
+ value: headerValue,
121
+ path,
122
+ defaults: cfg.secrets?.defaults,
123
+ mode: "inspect"
124
+ });
125
+ if (inspected.status === "available") {
126
+ headers.set(name, inspected.value);
127
+ continue;
128
+ }
129
+ if (inspected.status === "missing") continue;
130
+ const resolved = await resolveConfiguredSecretInputString({
131
+ config: cfg,
132
+ env: process.env,
133
+ value: headerValue,
134
+ path,
135
+ unresolvedReasonStyle: "detailed"
136
+ });
137
+ if (resolved.unresolvedRefReason) throw new Error(`${path} references an unavailable secret: ${resolved.unresolvedRefReason}`);
138
+ if (resolved.value) headers.set(name, resolved.value);
139
+ }
140
+ return headers;
141
+ }
110
142
  function resolveComfyNetworkPolicy(params) {
111
143
  let parsed;
112
144
  try {
@@ -138,8 +170,9 @@ async function readJsonResponse(params) {
138
170
  auditContext: params.auditContext
139
171
  });
140
172
  try {
141
- await assertOkOrThrowHttpError(response, params.errorPrefix);
142
- return await readProviderJsonResponse(response, params.errorPrefix);
173
+ const requestHeaders = params.init?.headers;
174
+ await assertOkOrThrowHttpError(response, params.errorPrefix, { requestHeaders });
175
+ return await readProviderJsonResponse(response, params.errorPrefix, { requestHeaders });
143
176
  } finally {
144
177
  await release();
145
178
  }
@@ -153,14 +186,9 @@ function resolveFileExtension(params) {
153
186
  if (dotIndex < 0 || dotIndex === fileName.length - 1) return "bin";
154
187
  return fileName.slice(dotIndex + 1);
155
188
  }
156
- function toBlobBytes(buffer) {
157
- const arrayBuffer = new ArrayBuffer(buffer.byteLength);
158
- new Uint8Array(arrayBuffer).set(buffer);
159
- return arrayBuffer;
160
- }
161
189
  async function uploadInputImage(params) {
162
190
  const form = new FormData();
163
- form.set("image", new Blob([toBlobBytes(params.image.buffer)], { type: params.image.mimeType }), normalizeOptionalString(params.image.fileName) || `input.${resolveFileExtension({ mimeType: params.image.mimeType })}`);
191
+ form.set("image", new Blob([bufferToBlobPart(params.image.buffer)], { type: params.image.mimeType }), normalizeOptionalString(params.image.fileName) || `input.${resolveFileExtension({ mimeType: params.image.mimeType })}`);
164
192
  form.set("type", "input");
165
193
  form.set("overwrite", "true");
166
194
  const headers = new Headers(params.headers);
@@ -230,7 +258,10 @@ async function waitForCloudCompletion(params) {
230
258
  errorPrefix: "Comfy status lookup failed"
231
259
  });
232
260
  if (status.status === "completed") return;
233
- if (status.status === "failed" || status.status === "cancelled") throw new Error(`Comfy workflow ${status.status}: ${status.error ?? status.message ?? params.promptId}`);
261
+ if (status.status === "failed" || status.status === "cancelled") {
262
+ const detail = redactProviderResponseErrorText(status.error ?? status.message ?? params.promptId, params.headers);
263
+ throw new Error(`Comfy workflow ${status.status}: ${detail}`);
264
+ }
234
265
  const pollDelayMs = resolveComfyRemainingMs(deadline, params.timeoutMs, params.pollIntervalMs);
235
266
  await new Promise((resolve) => {
236
267
  setTimeout(resolve, pollDelayMs);
@@ -290,7 +321,7 @@ async function downloadOutputFile(params) {
290
321
  auditContext
291
322
  });
292
323
  try {
293
- await assertOkOrThrowHttpError(firstResponse.response, "Comfy output download failed");
324
+ await assertOkOrThrowHttpError(firstResponse.response, "Comfy output download failed", { requestHeaders: params.headers });
294
325
  const mimeType = normalizeOptionalString(firstResponse.response.headers.get("content-type")) || "application/octet-stream";
295
326
  return {
296
327
  buffer: await readResponseWithLimit(firstResponse.response, params.maxBytes, {
@@ -304,11 +335,30 @@ async function downloadOutputFile(params) {
304
335
  await firstResponse.release();
305
336
  }
306
337
  }
338
+ function hasUnavailableComfyHeaderSecret(value, cfg) {
339
+ if (!isRecord(value)) return false;
340
+ return Object.entries(value).some(([name, headerValue]) => {
341
+ const inspected = resolveSecretInputString({
342
+ value: headerValue,
343
+ path: `plugins.entries.comfy.config.headers.${name}`,
344
+ defaults: cfg?.secrets?.defaults,
345
+ mode: "inspect"
346
+ });
347
+ if (inspected.status !== "configured_unavailable" || inspected.ref.source !== "env") return false;
348
+ const envVarName = inspected.ref.id.trim();
349
+ return !(canResolveEnvSecretRefInReadOnlyPath({
350
+ cfg,
351
+ provider: inspected.ref.provider,
352
+ id: envVarName
353
+ }) && Boolean(normalizeSecretInputString(process.env[envVarName])));
354
+ });
355
+ }
307
356
  function isComfyCapabilityConfigured(params) {
308
357
  const capabilityConfig = getComfyCapabilityConfig(getComfyConfig(params.cfg), params.capability);
309
358
  const hasWorkflow = Boolean(resolveComfyWorkflowSource(capabilityConfig).workflow || normalizeOptionalString(capabilityConfig.workflowPath));
310
359
  const hasPromptNode = Boolean(normalizeOptionalString(capabilityConfig.promptNodeId));
311
360
  if (!hasWorkflow || !hasPromptNode) return false;
361
+ if (hasUnavailableComfyHeaderSecret(capabilityConfig.headers, params.cfg)) return false;
312
362
  if (resolveComfyMode(capabilityConfig) === "local") return true;
313
363
  const configuredApiKey = resolveComfyApiKey(capabilityConfig, params.cfg);
314
364
  if (configuredApiKey.status === "available") return true;
@@ -327,6 +377,8 @@ async function runComfyWorkflow(params) {
327
377
  const promptInputName = normalizeOptionalString(capabilityConfig.promptInputName) ?? DEFAULT_PROMPT_INPUT_NAME;
328
378
  const inputImageNodeId = normalizeOptionalString(capabilityConfig.inputImageNodeId);
329
379
  const inputImageInputName = normalizeOptionalString(capabilityConfig.inputImageInputName) ?? DEFAULT_INPUT_IMAGE_INPUT_NAME;
380
+ const seedNodeId = normalizeOptionalString(capabilityConfig.seedNodeId);
381
+ const seedInputName = normalizeOptionalString(capabilityConfig.seedInputName) ?? DEFAULT_SEED_INPUT_NAME;
330
382
  const outputNodeId = normalizeOptionalString(capabilityConfig.outputNodeId);
331
383
  const pollIntervalMs = resolvePositiveTimerTimeoutMs(readConfigInteger(capabilityConfig, "pollIntervalMs"), DEFAULT_POLL_INTERVAL_MS);
332
384
  const timeoutMs = resolvePositiveTimerTimeoutMs(readConfigInteger(capabilityConfig, "timeoutMs") ?? params.timeoutMs, DEFAULT_TIMEOUT_MS);
@@ -337,6 +389,12 @@ async function runComfyWorkflow(params) {
337
389
  inputName: promptInputName,
338
390
  value: params.prompt
339
391
  });
392
+ if (seedNodeId) setWorkflowInput({
393
+ workflow,
394
+ nodeId: seedNodeId,
395
+ inputName: seedInputName,
396
+ value: randomInt(RANDOM_SEED_EXCLUSIVE_MAX)
397
+ });
340
398
  const pluginApiKey = resolveComfyApiKey(capabilityConfig, params.cfg);
341
399
  const resolvedAuth = mode === "cloud" ? pluginApiKey.status === "available" ? {
342
400
  apiKey: pluginApiKey.apiKey,
@@ -354,6 +412,7 @@ async function runComfyWorkflow(params) {
354
412
  baseUrl: normalizeOptionalString(capabilityConfig.baseUrl),
355
413
  defaultBaseUrl: mode === "cloud" ? DEFAULT_COMFY_CLOUD_BASE_URL : DEFAULT_COMFY_LOCAL_BASE_URL,
356
414
  allowPrivateNetwork: mode === "local" || explicitAllowPrivateNetwork,
415
+ headers: await resolveComfyHeadersConfig(capabilityConfig.headers, params.cfg),
357
416
  defaultHeaders: mode === "cloud" ? {
358
417
  "X-API-Key": resolvedAuth?.apiKey ?? "",
359
418
  "Content-Type": "application/json"
@@ -391,7 +450,7 @@ async function runComfyWorkflow(params) {
391
450
  prompt: workflow,
392
451
  ...mode === "cloud" && resolvedAuth?.apiKey ? { extra_data: { api_key_comfy_org: resolvedAuth.apiKey } } : {}
393
452
  };
394
- const promptId = normalizeOptionalString((await readJsonResponse({
453
+ const promptResponse = await readJsonResponse({
395
454
  url: `${normalizedBaseUrl}${mode === "cloud" ? "/api/prompt" : "/prompt"}`,
396
455
  init: {
397
456
  method: "POST",
@@ -403,7 +462,8 @@ async function runComfyWorkflow(params) {
403
462
  dispatcherPolicy,
404
463
  auditContext: `comfy-${params.capability}-generate`,
405
464
  errorPrefix: "Comfy workflow submit failed"
406
- })).prompt_id);
465
+ });
466
+ const promptId = normalizeOptionalString(promptResponse.prompt_id);
407
467
  if (!promptId) throw new Error("Comfy workflow submit response missing prompt_id");
408
468
  const historyEntry = extractHistoryEntry(mode === "cloud" ? await (async () => {
409
469
  await waitForCloudCompletion({
@@ -5,6 +5,11 @@
5
5
  },
6
6
  "enabledByDefault": true,
7
7
  "providers": ["comfy"],
8
+ "configContracts": {
9
+ "secretInputs": {
10
+ "paths": [{ "path": "headers.*", "expected": "string" }]
11
+ }
12
+ },
8
13
  "setup": {
9
14
  "providers": [
10
15
  {
@@ -196,6 +201,10 @@
196
201
  "allowPrivateNetwork": {
197
202
  "type": "boolean"
198
203
  },
204
+ "headers": {
205
+ "type": "object",
206
+ "additionalProperties": { "type": ["string", "object"] }
207
+ },
199
208
  "workflowPath": {
200
209
  "type": "string"
201
210
  },
@@ -208,6 +217,12 @@
208
217
  "promptInputName": {
209
218
  "type": "string"
210
219
  },
220
+ "seedNodeId": {
221
+ "type": "string"
222
+ },
223
+ "seedInputName": {
224
+ "type": "string"
225
+ },
211
226
  "inputImageNodeId": {
212
227
  "type": "string"
213
228
  },
@@ -233,6 +248,8 @@
233
248
  "workflow": { "type": "object" },
234
249
  "promptNodeId": { "type": "string" },
235
250
  "promptInputName": { "type": "string" },
251
+ "seedNodeId": { "type": "string" },
252
+ "seedInputName": { "type": "string" },
236
253
  "inputImageNodeId": { "type": "string" },
237
254
  "inputImageInputName": { "type": "string" },
238
255
  "outputNodeId": { "type": "string" },
@@ -248,6 +265,8 @@
248
265
  "workflow": { "type": "object" },
249
266
  "promptNodeId": { "type": "string" },
250
267
  "promptInputName": { "type": "string" },
268
+ "seedNodeId": { "type": "string" },
269
+ "seedInputName": { "type": "string" },
251
270
  "inputImageNodeId": { "type": "string" },
252
271
  "inputImageInputName": { "type": "string" },
253
272
  "outputNodeId": { "type": "string" },
@@ -263,6 +282,8 @@
263
282
  "workflow": { "type": "object" },
264
283
  "promptNodeId": { "type": "string" },
265
284
  "promptInputName": { "type": "string" },
285
+ "seedNodeId": { "type": "string" },
286
+ "seedInputName": { "type": "string" },
266
287
  "outputNodeId": { "type": "string" },
267
288
  "pollIntervalMs": { "type": "integer", "minimum": 100 },
268
289
  "timeoutMs": { "type": "integer", "minimum": 1000 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/comfy-provider",
3
- "version": "2026.9.1-beta.1",
3
+ "version": "2026.9.2",
4
4
  "description": "OpenClaw ComfyUI provider plugin.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,10 +18,10 @@
18
18
  "minHostVersion": ">=2026.7.2"
19
19
  },
20
20
  "compat": {
21
- "pluginApi": ">=2026.9.1-beta.1"
21
+ "pluginApi": ">=2026.9.2"
22
22
  },
23
23
  "build": {
24
- "openclawVersion": "2026.9.1-beta.1",
24
+ "openclawVersion": "2026.9.2",
25
25
  "bundledDist": false
26
26
  },
27
27
  "release": {
@@ -38,7 +38,7 @@
38
38
  "README.md"
39
39
  ],
40
40
  "peerDependencies": {
41
- "openclaw": ">=2026.9.1-beta.1"
41
+ "openclaw": ">=2026.9.2"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "openclaw": {