mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 21:56:51 +00:00
223 lines
6.7 KiB
JavaScript
223 lines
6.7 KiB
JavaScript
import { Agent, setGlobalDispatcher } from "undici";
|
|
|
|
const baseUrl = process.env.OPENWEBUI_BASE_URL ?? "";
|
|
const email = process.env.OPENWEBUI_ADMIN_EMAIL ?? "";
|
|
const password = process.env.OPENWEBUI_ADMIN_PASSWORD ?? "";
|
|
const expectedNonce = process.env.OPENWEBUI_EXPECTED_NONCE ?? "";
|
|
const prompt = process.env.OPENWEBUI_PROMPT ?? "";
|
|
const modelAttempts = readPositiveInt(process.env.OPENWEBUI_MODEL_ATTEMPTS, 72);
|
|
const modelRetryMs = readNonNegativeInt(process.env.OPENWEBUI_MODEL_RETRY_MS, 5000);
|
|
const fetchTimeoutMs = readPositiveInt(process.env.OPENWEBUI_FETCH_TIMEOUT_MS, 720000);
|
|
const controlTimeoutMs = readPositiveInt(
|
|
process.env.OPENWEBUI_CONTROL_TIMEOUT_MS,
|
|
Math.min(fetchTimeoutMs, 30000),
|
|
);
|
|
const chatTimeoutMs = readPositiveInt(process.env.OPENWEBUI_CHAT_TIMEOUT_MS, fetchTimeoutMs);
|
|
const smokeMode =
|
|
process.env.OPENWEBUI_SMOKE_MODE ?? process.env.OPENCLAW_OPENWEBUI_SMOKE_MODE ?? "chat";
|
|
|
|
setGlobalDispatcher(
|
|
new Agent({
|
|
bodyTimeout: Math.max(controlTimeoutMs, chatTimeoutMs),
|
|
headersTimeout: Math.max(controlTimeoutMs, chatTimeoutMs),
|
|
}),
|
|
);
|
|
|
|
if (!baseUrl || !email || !password || !expectedNonce || !prompt) {
|
|
throw new Error("Missing required OPENWEBUI_* environment variables");
|
|
}
|
|
if (smokeMode !== "models" && smokeMode !== "chat") {
|
|
throw new Error(`Unsupported OPENWEBUI_SMOKE_MODE: ${smokeMode}`);
|
|
}
|
|
|
|
function readPositiveInt(raw, fallback) {
|
|
const parsed = Number.parseInt(String(raw || ""), 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|
|
|
|
function readNonNegativeInt(raw, fallback) {
|
|
const parsed = Number.parseInt(String(raw || ""), 10);
|
|
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
}
|
|
|
|
function createTimeoutError(label, timeoutMs) {
|
|
const error = new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
error.code = "ETIMEDOUT";
|
|
return error;
|
|
}
|
|
|
|
async function withRequestTimeout(label, timeoutMs, run) {
|
|
const controller = new AbortController();
|
|
const timeoutError = createTimeoutError(label, timeoutMs);
|
|
const timer = setTimeout(() => {
|
|
controller.abort(timeoutError);
|
|
}, timeoutMs);
|
|
timer.unref?.();
|
|
try {
|
|
return await run(controller.signal);
|
|
} catch (error) {
|
|
if (controller.signal.aborted) {
|
|
throw timeoutError;
|
|
}
|
|
throw error;
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
function getCookieHeader(res) {
|
|
const raw = res.headers.get("set-cookie");
|
|
if (!raw) {
|
|
return "";
|
|
}
|
|
return raw
|
|
.split(/,(?=[^;]+=[^;]+)/g)
|
|
.map((part) => part.split(";", 1)[0]?.trim())
|
|
.filter(Boolean)
|
|
.join("; ");
|
|
}
|
|
|
|
function buildAuthHeaders(token, cookie) {
|
|
const headers = {};
|
|
if (token) {
|
|
headers.authorization = `Bearer ${token}`;
|
|
}
|
|
if (cookie) {
|
|
headers.cookie = cookie;
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, ms);
|
|
});
|
|
}
|
|
|
|
async function fetchSignin() {
|
|
return await withRequestTimeout("Open WebUI signin", controlTimeoutMs, async (signal) => {
|
|
const response = await fetch(`${baseUrl}/api/v1/auths/signin`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ email, password }),
|
|
signal,
|
|
});
|
|
if (!response.ok) {
|
|
const body = await response.text();
|
|
throw new Error(`signin failed: HTTP ${response.status} ${body}`);
|
|
}
|
|
return {
|
|
cookie: getCookieHeader(response),
|
|
json: await response.json(),
|
|
};
|
|
});
|
|
}
|
|
|
|
async function fetchModels(authHeaders, attempt) {
|
|
return await withRequestTimeout(
|
|
`Open WebUI models attempt ${attempt}`,
|
|
controlTimeoutMs,
|
|
async (signal) => {
|
|
const response = await fetch(`${baseUrl}/api/models`, { headers: authHeaders, signal });
|
|
if (!response.ok) {
|
|
return {
|
|
ok: false,
|
|
status: response.status,
|
|
text: await response.text(),
|
|
};
|
|
}
|
|
return {
|
|
json: await response.json(),
|
|
ok: true,
|
|
};
|
|
},
|
|
);
|
|
}
|
|
|
|
async function fetchChatCompletion(authHeaders, targetModel) {
|
|
return await withRequestTimeout("Open WebUI chat completion", chatTimeoutMs, async (signal) => {
|
|
const response = await fetch(`${baseUrl}/api/chat/completions`, {
|
|
method: "POST",
|
|
headers: {
|
|
...authHeaders,
|
|
"content-type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
model: targetModel,
|
|
messages: [{ role: "user", content: prompt }],
|
|
}),
|
|
signal,
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`/api/chat/completions failed: HTTP ${response.status} ${await response.text()}`,
|
|
);
|
|
}
|
|
return await response.json();
|
|
});
|
|
}
|
|
|
|
function extractModelIds(modelsJson) {
|
|
const models = Array.isArray(modelsJson)
|
|
? modelsJson
|
|
: Array.isArray(modelsJson?.data)
|
|
? modelsJson.data
|
|
: Array.isArray(modelsJson?.models)
|
|
? modelsJson.models
|
|
: [];
|
|
return models
|
|
.map((entry) => entry?.id ?? entry?.model ?? entry?.name)
|
|
.filter((value) => typeof value === "string");
|
|
}
|
|
|
|
const signin = await fetchSignin();
|
|
const signinJson = signin.json;
|
|
const token =
|
|
signinJson?.token ?? signinJson?.access_token ?? signinJson?.jwt ?? signinJson?.data?.token ?? "";
|
|
const authHeaders = {
|
|
...buildAuthHeaders(token, signin.cookie),
|
|
accept: "application/json",
|
|
};
|
|
|
|
let modelIds = [];
|
|
let targetModel = "";
|
|
let lastModelsError = "";
|
|
for (let attempt = 1; attempt <= modelAttempts; attempt += 1) {
|
|
const modelsResult = await fetchModels(authHeaders, attempt).catch((error) => {
|
|
lastModelsError = error instanceof Error ? error.message : String(error);
|
|
return undefined;
|
|
});
|
|
if (modelsResult?.ok) {
|
|
modelIds = extractModelIds(modelsResult.json);
|
|
targetModel =
|
|
modelIds.find((id) => id === "openclaw/default") ?? modelIds.find((id) => id === "openclaw");
|
|
if (targetModel) {
|
|
break;
|
|
}
|
|
lastModelsError = `missing openclaw model: ${JSON.stringify(modelIds)}`;
|
|
} else if (modelsResult) {
|
|
lastModelsError = `HTTP ${modelsResult.status} ${modelsResult.text}`;
|
|
}
|
|
if (attempt < modelAttempts) {
|
|
await sleep(modelRetryMs);
|
|
}
|
|
}
|
|
if (!targetModel) {
|
|
throw new Error(
|
|
`openclaw model missing from Open WebUI model list after retry: ${JSON.stringify(modelIds)} (${lastModelsError})`,
|
|
);
|
|
}
|
|
if (smokeMode === "models") {
|
|
console.log(JSON.stringify({ ok: true, mode: smokeMode, model: targetModel }, null, 2));
|
|
process.exit(0);
|
|
}
|
|
|
|
const chatJson = await fetchChatCompletion(authHeaders, targetModel);
|
|
const reply =
|
|
chatJson?.choices?.[0]?.message?.content ?? chatJson?.message?.content ?? chatJson?.content ?? "";
|
|
if (typeof reply !== "string" || !reply.includes(expectedNonce)) {
|
|
throw new Error(`chat reply missing nonce: ${JSON.stringify(reply)}`);
|
|
}
|
|
|
|
console.log(JSON.stringify({ ok: true, model: targetModel, reply }, null, 2));
|