openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
159 lines (158 loc) • 5.69 kB
JavaScript
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { r as normalizeProviderId } from "./provider-id-DMd-TDFp.js";
import { n as formatErrorMessageWithCode } from "./errors-Db3Ymjlb.js";
import { i as fetchWithSsrFGuard } from "./fetch-guard-BMdGQhbb.js";
import { t as isLocalProviderBaseUrl } from "./model-provider-local-D8W-EfDm.js";
//#region src/cron/isolated-agent/model-preflight.runtime.ts
/** Preflights local model-provider endpoints before scheduled cron runner startup. */
const PREFLIGHT_CACHE_TTL_MS = 3e5;
const PREFLIGHT_TIMEOUT_MS = 2500;
const MAX_PREFLIGHT_ERROR_CAUSE_DEPTH = 8;
const MAX_PREFLIGHT_ERROR_CHARS = 1e3;
const preflightCache = /* @__PURE__ */ new Map();
function resolveProviderConfig(cfg, provider) {
const providers = cfg.models?.providers;
if (!providers) return;
const direct = providers[provider];
if (direct) return direct;
const normalized = normalizeProviderId(provider);
return Object.entries(providers).find(([key]) => normalizeProviderId(key) === normalized)?.[1];
}
function normalizeBaseUrl(value) {
if (typeof value !== "string") return;
const trimmed = value.trim().replace(/\/+$/, "");
return trimmed ? trimmed : void 0;
}
function normalizeProbeApi(providerConfig) {
const api = normalizeLowercaseStringOrEmpty(providerConfig.api);
return api === "ollama" || api === "openai-completions" ? api : void 0;
}
function buildProbeUrl(api, baseUrl) {
if (api === "ollama") return `${baseUrl}/api/tags`;
return `${baseUrl}/models`;
}
function buildLocalProviderSsrFPolicy(baseUrl) {
try {
const parsed = new URL(baseUrl);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
return {
hostnameAllowlist: [parsed.hostname],
allowPrivateNetwork: true
};
} catch {
return;
}
}
function readErrorProperty(error, key) {
if (typeof error !== "object" && typeof error !== "function" || error === null) return;
try {
return error[key];
} catch {
return;
}
}
function collectPreflightErrorCauseChain(error) {
const chain = [];
const seen = /* @__PURE__ */ new Set();
let current = error;
while (current !== void 0 && current !== null && chain.length < MAX_PREFLIGHT_ERROR_CAUSE_DEPTH && !seen.has(current)) {
seen.add(current);
chain.push(current);
current = readErrorProperty(current, "cause");
}
return chain;
}
function formatPreflightError(error) {
const causeChain = collectPreflightErrorCauseChain(error);
const causeDetails = formatErrorMessageWithCode(error);
const classified = causeChain.some((candidate) => readErrorProperty(candidate, "name") === "TimeoutError") ? `Local provider preflight exceeded its configured ${PREFLIGHT_TIMEOUT_MS}ms deadline | ${causeDetails}` : causeDetails;
return classified.length <= MAX_PREFLIGHT_ERROR_CHARS ? classified : `${truncateUtf16Safe(classified, 999)}…`;
}
function formatUnavailableReason(params) {
return [
`This automation uses ${params.provider}/${params.model} but the local provider preflight failed at ${params.baseUrl}.`,
`The candidate is unavailable for this run; OpenClaw will retry its provider preflight on a later scheduled run.`,
`Last error: ${formatPreflightError(params.error)}`
].join(" ");
}
function buildUnavailableResult(params) {
return {
status: "unavailable",
provider: params.provider,
model: params.model,
baseUrl: params.baseUrl,
retryAfterMs: PREFLIGHT_CACHE_TTL_MS,
reason: formatUnavailableReason({
provider: params.provider,
model: params.model,
baseUrl: params.baseUrl,
error: params.error
})
};
}
async function probeLocalProviderEndpoint(params) {
const { response, release } = await fetchWithSsrFGuard({
url: buildProbeUrl(params.api, params.baseUrl),
init: { method: "GET" },
policy: buildLocalProviderSsrFPolicy(params.baseUrl),
timeoutMs: PREFLIGHT_TIMEOUT_MS,
auditContext: "cron-model-provider-preflight"
});
try {
response.status;
} finally {
if (!response.bodyUsed) response.body?.cancel().catch(() => void 0);
await release();
}
}
/** Checks local model-provider reachability before a scheduled cron run starts. */
async function preflightCronModelProvider(params) {
const providerConfig = resolveProviderConfig(params.cfg, params.provider);
if (!providerConfig) return { status: "available" };
const baseUrl = normalizeBaseUrl(providerConfig.baseUrl);
const api = normalizeProbeApi(providerConfig);
if (!baseUrl || !api || !isLocalProviderBaseUrl(baseUrl)) return { status: "available" };
const nowMs = params.nowMs ?? Date.now();
const cacheKey = `${api}\0${baseUrl}`;
const cached = preflightCache.get(cacheKey);
if (cached && nowMs - cached.checkedAtMs < PREFLIGHT_CACHE_TTL_MS) {
if (cached.result.status === "available") return { status: "available" };
return buildUnavailableResult({
provider: params.provider,
model: params.model,
baseUrl,
error: cached.result.error
});
}
let result;
try {
await probeLocalProviderEndpoint({
api,
baseUrl
});
result = { status: "available" };
} catch (error) {
result = {
status: "unavailable",
error
};
}
preflightCache.set(cacheKey, {
checkedAtMs: nowMs,
result
});
if (result.status === "available") return { status: "available" };
return buildUnavailableResult({
provider: params.provider,
model: params.model,
baseUrl,
error: result.error
});
}
/** Clears the local-provider preflight cache for deterministic tests. */
function resetCronModelProviderPreflightCacheForTest() {
preflightCache.clear();
}
//#endregion
export { preflightCronModelProvider, resetCronModelProviderPreflightCacheForTest };