Merge feat/runpod-serverless-workers into feat/dynamic-image-list-queue

This commit is contained in:
Robert Wojciechowski
2026-10-13 21:33:28 +00:00
17 changed files with 407 additions and 116 deletions
+16 -1
View File
@@ -15,7 +15,8 @@ import { parseHostInput, getMasterUrl as buildMasterUrl } from './urlUtils.js';
const WORKERS_CHANGED_EVENT = "distributed:workers-changed";
class DistributedExtension {
// Exported for tests; ComfyUI only uses the registerExtension side effect below.
export class DistributedExtension {
constructor() {
this.config = null;
this.originalQueuePrompt = api.queuePrompt.bind(api);
@@ -196,6 +197,7 @@ class DistributedExtension {
async updateWorkerEnabled(workerId, enabled) {
const worker = this.config.workers.find(w => w.id === workerId);
const previousEnabled = worker ? Boolean(worker.enabled) : !enabled;
if (worker) {
worker.enabled = enabled;
this.state.updateWorker(workerId, { enabled });
@@ -219,6 +221,19 @@ class DistributedExtension {
await this.api.updateWorker(workerId, { enabled });
} catch (error) {
this.log("Error updating worker: " + error.message, "error");
// The backend kept the old state (e.g. a serverless worker without a
// saved API key); show that instead of a toggle that did not stick.
if (worker) {
worker.enabled = previousEnabled;
this.state.updateWorker(workerId, { enabled: previousEnabled });
this._emitWorkersChanged();
}
this.app?.extensionManager?.toast?.add({
severity: "error",
summary: enabled ? "Could not enable worker" : "Could not disable worker",
detail: error.message,
life: 5000,
});
}
if (this.panelElement) {
+129 -14
View File
@@ -7,7 +7,7 @@ import { createWorkerSettingsForm } from "../ui/settingsForm.js";
import { DistributedUI } from "../ui.js";
import { checkWorkerStatus, formatServerlessStatus, getWorkerUrl, launchWorker, stopWorker, viewWorkerLog } from "../workerLifecycle.js";
import { handleClearMemory, handleInterruptWorkers } from "../workerUtils.js";
import { isRemoteWorker, saveWorkerSettings, testServerlessWorker } from "../workerSettings.js";
import { isRemoteWorker, normalizeRunpodEndpointId, saveWorkerSettings, testServerlessWorker } from "../workerSettings.js";
import { performPreflightCheck } from "../executionUtils.js";
function createDocumentMock() {
@@ -125,12 +125,14 @@ describe("Runpod serverless worker panel", () => {
expect(elements.get(`${field}-group-saved`).style.display).toBe("none");
}
expect(elements.get("runpod-text-saved").style.display).toBe("none");
for (const field of ["endpoint-id", "api-key", "clear-api-key", "jobs-per-run", "concurrency", "timeout-seconds", "serverless-test"]) {
for (const field of ["endpoint-id", "api-key", "clear-api-key", "jobs-per-run", "timeout-seconds", "serverless-test"]) {
expect(elements.get(`${field}-group-saved`).style.display).toBe("flex");
}
// The separate concurrency setting was removed; a legacy value is ignored.
expect(elements.has("concurrency-saved")).toBe(false);
expect(elements.has("concurrency-group-saved")).toBe(false);
expect(elements.get("endpoint-id-saved").value).toBe("ep_123");
expect(elements.get("jobs-per-run-saved").value).toBe("2");
expect(elements.get("concurrency-saved").value).toBe("1");
expect(elements.get("timeout-seconds-saved").value).toBe("900");
expect(elements.get("api-key-saved").value).toBe("");
expect(elements.get("api-key-saved").placeholder).toMatch(/leave blank/i);
@@ -170,10 +172,11 @@ describe("Runpod serverless worker panel", () => {
for (const id of ["host-group-w1", "port-group-w1", "cuda-group-w1", "args-group-w1"]) {
expect(elements.get(id).style.display).toBe("none");
}
for (const id of ["endpoint-id-w1", "api-key-w1", "jobs-per-run-w1", "concurrency-w1", "timeout-seconds-w1"]) {
for (const id of ["endpoint-id-w1", "api-key-w1", "jobs-per-run-w1", "timeout-seconds-w1"]) {
expect(elements.has(id)).toBe(true);
}
for (const id of ["endpoint-id-group-w1", "api-key-group-w1", "clear-api-key-group-w1", "jobs-per-run-group-w1", "concurrency-group-w1", "timeout-seconds-group-w1", "serverless-test-group-w1"]) {
expect(elements.has("concurrency-w1")).toBe(false);
for (const id of ["endpoint-id-group-w1", "api-key-group-w1", "clear-api-key-group-w1", "jobs-per-run-group-w1", "timeout-seconds-group-w1", "serverless-test-group-w1"]) {
expect(elements.get(id).style.display).toBe("flex");
}
expect(elements.has("serverless-help-w1")).toBe(false);
@@ -183,15 +186,32 @@ describe("Runpod serverless worker panel", () => {
expect(elements.get("jobs-per-run-w1").value).toBe("1");
expect(elements.get("jobs-per-run-w1").min).toBe("1");
expect(elements.get("jobs-per-run-w1").max).toBe("16");
expect(elements.get("concurrency-w1").value).toBe("1");
expect(elements.get("concurrency-w1").min).toBe("1");
expect(elements.get("concurrency-w1").max).toBe("16");
expect(elements.get("jobs-per-run-w1").title).toBe(
"Separate Runpod jobs submitted each run, all at once. Runpod's max workers decides how many GPUs actually run them.",
);
expect(elements.get("timeout-seconds-w1").value).toBe("1800");
expect(elements.get("timeout-seconds-w1").min).toBe("30");
expect(elements.get("timeout-seconds-w1").max).toBe("86400");
expect(form.children.length).toBeGreaterThan(0);
});
it("disables Clear saved API key while the worker is enabled and explains why", () => {
vi.spyOn(document, "getElementById").mockReturnValue(null);
const base = { name: "Runpod", type: "serverless", endpoint_id: "ep_123" };
createWorkerSettingsForm(createFormUi(), {}, { ...base, id: "on", enabled: true, api_key_configured: true });
createWorkerSettingsForm(createFormUi(), {}, { ...base, id: "off", enabled: false, api_key_configured: true });
createWorkerSettingsForm(createFormUi(), {}, { ...base, id: "nokey", enabled: false, api_key_configured: false });
expect(elements.get("clear-api-key-on").disabled).toBe(true);
expect(elements.get("clear-api-key-group-on").title).toMatch(/turn this worker off/i);
expect(elements.get("clear-api-key-off").disabled).toBe(false);
expect(elements.get("clear-api-key-nokey").disabled).toBe(true);
expect(elements.get("clear-api-key-group-nokey").title).toMatch(/no api key is saved/i);
for (const id of ["on", "off", "nokey"]) {
expect(elements.get(`clear-api-key-group-${id}`).title).not.toMatch(/pool/i);
}
});
it("saves fixed Runpod settings without enabling or retaining the API key", async () => {
const worker = { id: "w2", name: "Old", type: "remote", enabled: false, api_key_configured: false };
const extension = makeSaveExtension(worker);
@@ -201,14 +221,13 @@ describe("Runpod serverless worker panel", () => {
elements.set("api-key-w2", { value: "secret-token" });
elements.set("clear-api-key-w2", { checked: false });
elements.set("jobs-per-run-w2", { value: "4" });
elements.set("concurrency-w2", { value: "3" });
elements.set("timeout-seconds-w2", { value: "2400" });
await saveWorkerSettings(extension, "w2");
expect(extension.api.updateWorker).toHaveBeenCalledWith("w2", {
name: "Runpod worker", type: "serverless", provider: "runpod", endpoint_id: "ep_123",
api_key: "secret-token", jobs_per_run: 4, concurrency: 3, timeout_seconds: 2400, enabled: false,
api_key: "secret-token", jobs_per_run: 4, timeout_seconds: 2400, enabled: false,
});
expect(worker).toMatchObject({ type: "serverless", provider: "runpod", enabled: false, api_key_configured: true });
expect(worker).not.toHaveProperty("api_key");
@@ -218,6 +237,104 @@ describe("Runpod serverless worker panel", () => {
expect(JSON.stringify(extension.state.updateWorker.mock.calls)).not.toContain("secret-token");
});
it("labels the single parallelism setting Images per run and drops a legacy concurrency value", async () => {
const worker = { id: "legacy", name: "Runpod", type: "serverless", provider: "runpod", enabled: false,
api_key_configured: true, jobs_per_run: 1, concurrency: 1 };
const extension = makeSaveExtension(worker);
elements.set("name-legacy", { value: "Runpod" });
elements.set("worker-type-legacy", { value: "serverless" });
elements.set("endpoint-id-legacy", { value: "ep_123" });
elements.set("api-key-legacy", { value: "" });
elements.set("clear-api-key-legacy", { checked: false });
elements.set("jobs-per-run-legacy", { value: "2" });
elements.set("timeout-seconds-legacy", { value: "1800" });
await saveWorkerSettings(extension, "legacy");
const payload = extension.api.updateWorker.mock.calls[0][1];
expect(payload.jobs_per_run).toBe(2);
expect(payload).not.toHaveProperty("concurrency");
expect(worker.jobs_per_run).toBe(2);
expect(worker).not.toHaveProperty("concurrency");
elements.set("jobs-per-run-legacy", { value: "17" });
await saveWorkerSettings(extension, "legacy");
expect(extension.api.updateWorker).toHaveBeenCalledTimes(1);
expect(extension.app.extensionManager.toast.add).toHaveBeenLastCalledWith(
expect.objectContaining({ detail: "Images per run must be a whole number from 1 to 16" }),
);
});
it("trims a pasted Runpod URL to the endpoint ID and explains other invalid IDs", async () => {
expect(normalizeRunpodEndpointId(" abc123xyz ")).toBe("abc123xyz");
expect(normalizeRunpodEndpointId("https://api.runpod.ai/v2/abc123xyz/run")).toBe("abc123xyz");
expect(normalizeRunpodEndpointId("api.runpod.ai/v2/abc_12-3")).toBe("abc_12-3");
expect(normalizeRunpodEndpointId("https://www.runpod.io/console/serverless/user/endpoint/abc")).toBe(
"https://www.runpod.io/console/serverless/user/endpoint/abc",
);
const worker = { id: "url", name: "Runpod", type: "serverless", provider: "runpod", enabled: false, api_key_configured: true };
const extension = makeSaveExtension(worker);
elements.set("name-url", { value: "Runpod" });
elements.set("worker-type-url", { value: "serverless" });
elements.set("endpoint-id-url", { value: "https://api.runpod.ai/v2/abc123xyz/runsync" });
elements.set("api-key-url", { value: "" });
elements.set("clear-api-key-url", { checked: false });
elements.set("jobs-per-run-url", { value: "1" });
elements.set("timeout-seconds-url", { value: "1800" });
await saveWorkerSettings(extension, "url");
expect(extension.api.updateWorker.mock.calls[0][1].endpoint_id).toBe("abc123xyz");
elements.set("endpoint-id-url", { value: "https://www.runpod.io/console/serverless/endpoint/abc" });
await saveWorkerSettings(extension, "url");
expect(extension.api.updateWorker).toHaveBeenCalledTimes(1);
expect(extension.app.extensionManager.toast.add).toHaveBeenLastCalledWith(
expect.objectContaining({ detail: expect.stringMatching(/just the Runpod endpoint ID/) }),
);
});
it("asks before a type change deletes the saved API key, and cancels cleanly", async () => {
const worker = { id: "conv", name: "Runpod", type: "serverless", provider: "runpod", enabled: false, api_key_configured: true };
const extension = makeSaveExtension(worker);
elements.set("name-conv", { value: "Runpod" });
elements.set("worker-type-conv", { value: "remote" });
elements.set("host-conv", { value: "192.168.1.20" });
elements.set("port-conv", { value: "8188" });
const confirmSpy = vi.fn().mockReturnValue(false);
globalThis.confirm = confirmSpy;
try {
await saveWorkerSettings(extension, "conv");
expect(confirmSpy).toHaveBeenCalledWith(expect.stringMatching(/saved Runpod API key will be deleted/));
expect(extension.api.updateWorker).not.toHaveBeenCalled();
expect(worker.type).toBe("serverless");
confirmSpy.mockReturnValue(true);
await saveWorkerSettings(extension, "conv");
expect(extension.api.updateWorker).toHaveBeenCalledTimes(1);
expect(extension.api.updateWorker.mock.calls[0][1].type).toBe("remote");
} finally {
delete globalThis.confirm;
}
});
it("does not ask for confirmation when a serverless worker without a key changes type", async () => {
const worker = { id: "nokey2", name: "Runpod", type: "serverless", provider: "runpod", enabled: false, api_key_configured: false };
const extension = makeSaveExtension(worker);
elements.set("name-nokey2", { value: "Runpod" });
elements.set("worker-type-nokey2", { value: "remote" });
elements.set("host-nokey2", { value: "192.168.1.20" });
elements.set("port-nokey2", { value: "8188" });
const confirmSpy = vi.fn().mockReturnValue(false);
globalThis.confirm = confirmSpy;
try {
await saveWorkerSettings(extension, "nokey2");
expect(confirmSpy).not.toHaveBeenCalled();
expect(extension.api.updateWorker).toHaveBeenCalledTimes(1);
} finally {
delete globalThis.confirm;
}
});
it("omits a blank API key so the saved key is preserved", async () => {
const worker = { id: "w-blank", name: "Runpod", type: "serverless", provider: "runpod", enabled: true, api_key_configured: true };
const extension = makeSaveExtension(worker);
@@ -227,7 +344,6 @@ describe("Runpod serverless worker panel", () => {
elements.set("api-key-w-blank", { value: "" });
elements.set("clear-api-key-w-blank", { checked: false });
elements.set("jobs-per-run-w-blank", { value: "1" });
elements.set("concurrency-w-blank", { value: "1" });
elements.set("timeout-seconds-w-blank", { value: "1800" });
await saveWorkerSettings(extension, "w-blank");
@@ -248,14 +364,13 @@ describe("Runpod serverless worker panel", () => {
elements.set("api-key-w3", { value: "" });
elements.set("clear-api-key-w3", { checked: true });
elements.set("jobs-per-run-w3", { value: "1" });
elements.set("concurrency-w3", { value: "1" });
elements.set("timeout-seconds-w3", { value: "1800" });
await saveWorkerSettings(extension, "w3");
expect(extension.api.updateWorker).toHaveBeenCalledWith("w3", {
name: "Runpod", type: "serverless", provider: "runpod", endpoint_id: "ep_123",
clear_api_key: true, jobs_per_run: 1, concurrency: 1, timeout_seconds: 1800, enabled: true,
clear_api_key: true, jobs_per_run: 1, timeout_seconds: 1800, enabled: true,
});
expect(worker.api_key_configured).toBe(false);
expect(worker).not.toHaveProperty("api_key");
@@ -300,7 +415,7 @@ describe("Runpod serverless worker panel", () => {
expect(elements.get("serverless-status-compact").textContent).toBe("Queued");
expect(elements.get("endpoint-id-compact").value).toBe("ep_private_id");
expect(Number(elements.get("jobs-per-run-compact").value)).toBe(1);
expect(Number(elements.get("concurrency-compact").value)).toBe(1);
expect(elements.has("concurrency-compact")).toBe(false);
const box = elements.get("controls-compact").children[0].children[0];
expect(box.textContent).toBe("Runpod Serverless endpoint");
expect(box.classList.values.has("dist-info-box")).toBe(true);
+54
View File
@@ -0,0 +1,54 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../scripts/app.js", () => ({ app: { registerExtension: vi.fn() } }), { virtual: true });
vi.mock("../../scripts/api.js", () => ({ api: { clientId: "test-client", queuePrompt: vi.fn() } }), { virtual: true });
import { DistributedExtension } from "../main.js";
function makeExtension(worker, updateWorker) {
return {
config: { workers: [worker] },
state: { updateWorker: vi.fn() },
api: { updateWorker },
app: { extensionManager: { toast: { add: vi.fn() } } },
ui: { updateStatusDot: vi.fn() },
log: vi.fn(),
_emitWorkersChanged: vi.fn(),
panelElement: null,
};
}
describe("worker enable toggle", () => {
let originalDocument;
beforeEach(() => {
originalDocument = globalThis.document;
globalThis.document = { getElementById: () => null };
});
afterEach(() => {
globalThis.document = originalDocument;
});
it("reverts the toggle and explains why when the backend rejects the change", async () => {
const worker = { id: "w1", type: "serverless", enabled: false };
const extension = makeExtension(worker, vi.fn().mockRejectedValue(new Error("Save an API key before enabling this worker")));
await DistributedExtension.prototype.updateWorkerEnabled.call(extension, "w1", true);
expect(worker.enabled).toBe(false);
expect(extension.state.updateWorker).toHaveBeenLastCalledWith("w1", { enabled: false });
expect(extension.app.extensionManager.toast.add).toHaveBeenCalledWith(expect.objectContaining({
severity: "error", summary: "Could not enable worker", detail: "Save an API key before enabling this worker",
}));
});
it("keeps the new state when the backend accepts it", async () => {
const worker = { id: "w2", type: "serverless", enabled: false };
const extension = makeExtension(worker, vi.fn().mockResolvedValue({ status: "success" }));
await DistributedExtension.prototype.updateWorkerEnabled.call(extension, "w2", true);
expect(worker.enabled).toBe(true);
expect(extension.state.updateWorker).toHaveBeenLastCalledWith("w2", { enabled: true });
expect(extension.app.extensionManager.toast.add).not.toHaveBeenCalled();
});
});
+17 -13
View File
@@ -1,6 +1,9 @@
import { BUTTON_STYLES } from '../constants.js';
import { cancelWorkerSettings, deleteWorker, isRemoteWorker, saveWorkerSettings, testServerlessWorker } from '../workerSettings.js';
export const IMAGES_PER_RUN_TOOLTIP =
"Separate Runpod jobs submitted each run, all at once. Runpod's max workers decides how many GPUs actually run them.";
export function createWorkerSettingsForm(ui, extension, worker) {
const form = document.createElement("div");
form.style.cssText = "display: flex; flex-direction: column; gap: 8px;";
@@ -59,7 +62,6 @@ export function createWorkerSettingsForm(ui, extension, worker) {
apiKeyGroup.group,
clearApiKeyGroup,
jobsPerRunGroup.group,
concurrencyGroup.group,
timeoutGroup.group,
testServerlessGroup,
];
@@ -150,7 +152,17 @@ export function createWorkerSettingsForm(ui, extension, worker) {
clearApiKeyInput.id = `clear-api-key-${worker.id}`;
const clearApiKeyLabel = document.createElement("label");
clearApiKeyLabel.htmlFor = clearApiKeyInput.id;
clearApiKeyLabel.textContent = "Clear saved API key (disable pool first)";
clearApiKeyLabel.textContent = "Clear saved API key";
// The backend rejects clearing the key of an enabled worker; say so up front.
// The form is rebuilt when the enable toggle changes, so this stays current.
const clearApiKeyBlockedReason = !worker.api_key_configured
? "No API key is saved for this worker."
: worker.enabled
? "Turn this worker off before clearing its API key. An enabled serverless worker needs a saved key."
: "";
clearApiKeyInput.disabled = Boolean(clearApiKeyBlockedReason);
clearApiKeyGroup.title = clearApiKeyBlockedReason || "Remove the saved Runpod API key when you save.";
clearApiKeyLabel.style.cssText = clearApiKeyBlockedReason ? "opacity: 0.5; cursor: not-allowed;" : "cursor: pointer;";
clearApiKeyGroup.appendChild(clearApiKeyInput);
clearApiKeyGroup.appendChild(clearApiKeyLabel);
clearApiKeyInput.onchange = () => {
@@ -159,24 +171,16 @@ export function createWorkerSettingsForm(ui, extension, worker) {
};
form.appendChild(clearApiKeyGroup);
const jobsPerRunGroup = ui.createFormGroup("Jobs per run:", worker.jobs_per_run ?? 1, `jobs-per-run-${worker.id}`, "number");
const jobsPerRunGroup = ui.createFormGroup("Images per run (1–16):", worker.jobs_per_run ?? 1, `jobs-per-run-${worker.id}`, "number");
jobsPerRunGroup.group.id = `jobs-per-run-group-${worker.id}`;
jobsPerRunGroup.input.min = "1";
jobsPerRunGroup.input.max = "16";
jobsPerRunGroup.input.step = "1";
jobsPerRunGroup.input.title = "Maximum jobs submitted in one run";
jobsPerRunGroup.input.title = IMAGES_PER_RUN_TOOLTIP;
jobsPerRunGroup.group.title = IMAGES_PER_RUN_TOOLTIP;
jobsPerRunGroup.group.style.display = "none";
form.appendChild(jobsPerRunGroup.group);
const concurrencyGroup = ui.createFormGroup("Concurrent jobs (slots):", worker.concurrency ?? 1, `concurrency-${worker.id}`, "number");
concurrencyGroup.group.id = `concurrency-group-${worker.id}`;
concurrencyGroup.input.min = "1";
concurrencyGroup.input.max = "16";
concurrencyGroup.input.step = "1";
concurrencyGroup.input.title = "Maximum concurrent jobs per serverless worker";
concurrencyGroup.group.style.display = "none";
form.appendChild(concurrencyGroup.group);
const timeoutGroup = ui.createFormGroup("Timeout (seconds):", worker.timeout_seconds ?? 1800, `timeout-seconds-${worker.id}`, "number");
timeoutGroup.group.id = `timeout-seconds-group-${worker.id}`;
timeoutGroup.input.min = "30";
+31 -10
View File
@@ -37,6 +37,19 @@ export function isCloudWorker(extension, worker) {
return worker.type === "cloud";
}
// Mirrors the backend rule in utils/serverless_config.py.
const RUNPOD_ENDPOINT_ID_RE = /^[A-Za-z0-9_-]{1,200}$/;
/**
* Accept a pasted Runpod API URL (https://api.runpod.ai/v2/<id>/...) and keep
* only the endpoint ID; anything else is returned trimmed for validation.
*/
export function normalizeRunpodEndpointId(value) {
const text = String(value || "").trim();
const match = text.match(/^(?:https?:\/\/)?api\.runpod\.ai\/v2\/([A-Za-z0-9_-]{1,200})(?:[/?#].*)?$/i);
return match ? match[1] : text;
}
function showServerlessValidationError(extension, detail) {
extension.app.extensionManager.toast.add({
severity: "error",
@@ -47,11 +60,10 @@ function showServerlessValidationError(extension, detail) {
}
async function saveServerlessWorkerSettings(extension, worker, workerId, name) {
const endpointId = String(document.getElementById(`endpoint-id-${workerId}`).value || "").trim();
const endpointId = normalizeRunpodEndpointId(document.getElementById(`endpoint-id-${workerId}`).value);
const apiKey = document.getElementById(`api-key-${workerId}`).value || "";
const clearApiKey = Boolean(document.getElementById(`clear-api-key-${workerId}`).checked);
const jobsPerRun = Number(document.getElementById(`jobs-per-run-${workerId}`).value);
const concurrency = Number(document.getElementById(`concurrency-${workerId}`).value);
const timeoutSeconds = Number(document.getElementById(`timeout-seconds-${workerId}`).value);
if (!name.trim()) {
@@ -62,12 +74,15 @@ async function saveServerlessWorkerSettings(extension, worker, workerId, name) {
showServerlessValidationError(extension, "Endpoint ID is required for Runpod serverless workers");
return;
}
if (!Number.isInteger(jobsPerRun) || jobsPerRun < 1 || jobsPerRun > 16) {
showServerlessValidationError(extension, "Jobs per run must be an integer between 1 and 16");
if (!RUNPOD_ENDPOINT_ID_RE.test(endpointId)) {
showServerlessValidationError(
extension,
"Enter just the Runpod endpoint ID (for example abc123xyz), not a URL. It is shown on the endpoint's page in the Runpod console.",
);
return;
}
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 16) {
showServerlessValidationError(extension, "Concurrency must be an integer between 1 and 16");
if (!Number.isInteger(jobsPerRun) || jobsPerRun < 1 || jobsPerRun > 16) {
showServerlessValidationError(extension, "Images per run must be a whole number from 1 to 16");
return;
}
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 30 || timeoutSeconds > 86400) {
@@ -83,7 +98,6 @@ async function saveServerlessWorkerSettings(extension, worker, workerId, name) {
...(apiKey.length > 0 && !clearApiKey ? { api_key: apiKey } : {}),
...(clearApiKey ? { clear_api_key: true } : {}),
jobs_per_run: jobsPerRun,
concurrency,
timeout_seconds: timeoutSeconds,
// Never enable a serverless worker as a side effect of saving credentials.
enabled: worker.enabled,
@@ -98,7 +112,8 @@ async function saveServerlessWorkerSettings(extension, worker, workerId, name) {
worker.provider = "runpod";
worker.endpoint_id = endpointId;
worker.jobs_per_run = jobsPerRun;
worker.concurrency = concurrency;
// Replaced by a fixed backend limit; drop any value loaded from an older config.
delete worker.concurrency;
worker.timeout_seconds = timeoutSeconds;
worker.api_key_configured = clearApiKey
? false
@@ -178,6 +193,14 @@ export async function saveWorkerSettings(extension, workerId) {
return;
}
const name = document.getElementById(`name-${workerId}`).value;
if (worker.type === "serverless" && worker.api_key_configured) {
// The backend deletes the stored key when a worker stops being serverless.
const proceed = confirm(
`Change "${worker.name}" from Serverless (Runpod) to another type?\n\n` +
"Its saved Runpod API key will be deleted. You would need to enter it again to switch back."
);
if (!proceed) return;
}
const isRemote = workerType === "remote" || workerType === "cloud";
const isCloud = workerType === "cloud";
const rawHost = isRemote ? document.getElementById(`host-${workerId}`).value : window.location.hostname;
@@ -352,8 +375,6 @@ export function cancelWorkerSettings(extension, workerId) {
if (clearApiKeyInput) clearApiKeyInput.checked = false;
const jobsPerRunInput = document.getElementById(`jobs-per-run-${workerId}`);
if (jobsPerRunInput) jobsPerRunInput.value = worker.jobs_per_run ?? 1;
const concurrencyInput = document.getElementById(`concurrency-${workerId}`);
if (concurrencyInput) concurrencyInput.value = worker.concurrency ?? 1;
const timeoutInput = document.getElementById(`timeout-seconds-${workerId}`);
if (timeoutInput) timeoutInput.value = worker.timeout_seconds ?? 1800;