openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
488 lines (487 loc) • 22.5 kB
JavaScript
import { l as toErrorObject } from "./error-coercion-D_-xJ90S.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { m as readProviderJsonResponse } from "./provider-http-errors-U-nhuk_f.js";
import { i as fetchWithSsrFGuard } from "./fetch-guard-BMdGQhbb.js";
import { t as normalizeOptionalSecretInput } from "./normalize-secret-input-Df_qhWv_.js";
import { c as isNonSecretApiKeyMarker } from "./model-auth-markers-jBKQn38x.js";
import "./provider-auth-BeZ7NZUU.js";
import { c as validateApiKeyInput, i as normalizeApiKeyInput, n as ensureApiKeyFromOptionEnvOrPrompt } from "./provider-auth-input-B4MRIGbK.js";
import "./error-runtime-Bz9Tw57Z.js";
import { t as expectDefined } from "./expect-runtime-CJBt0Gq2.js";
import { n as WizardCancelledError } from "./prompts-DLsO8MlU.js";
import { n as applyAgentDefaultModelPrimary } from "./provider-onboard-D4DWEIGi.js";
import "./setup-BosJMgvw.js";
import "./ssrf-runtime-Bum5C6NN.js";
import "./provider-http-k9RMI7iG.js";
import { a as OLLAMA_DEFAULT_API_KEY, m as resolveOllamaSetupDefaultBaseUrl, n as OLLAMA_CLOUD_BASE_URL, r as OLLAMA_CLOUD_DEFAULT_MODELS, u as OLLAMA_DEFAULT_MODEL } from "./defaults-BiE2_Zq0.js";
import { t as readProviderBaseUrl } from "./provider-base-url-E6aWTKii.js";
import { _ as queryOllamaModelShowInfo, c as enrichOllamaModelsWithContext, d as isOllamaCloudModel, f as isOllamaEmbeddingOnlyModel, i as buildOllamaProvider, n as buildOllamaBaseUrlSsrFPolicy, u as fetchOllamaModels, y as resolveOllamaApiBase } from "./provider-models-BK60TdeT.js";
import "./discovery-shared-BZtIqpM2.js";
import { a as inspectOllamaModelsForSetup, i as findAvailableOllamaModelName, l as selectAppGuidedOllamaModelFromDiscovery, n as buildOllamaModelsConfig, o as mergeUniqueModelNames, r as discoverOllamaModelsForSetup, s as normalizeOllamaModelName } from "./setup-model-selection-BImV-Os6.js";
import { t as checkNdjsonRecordCap } from "./stream-ndjson-cap-DcEMmBe-.js";
//#region extensions/ollama/src/setup-pull.ts
const OLLAMA_PULL_RESPONSE_TIMEOUT_MS = 3e4;
const OLLAMA_PULL_STREAM_IDLE_TIMEOUT_MS = 3e5;
function formatOllamaPullStatus(status) {
const trimmed = status.trim();
const partStatusMatch = trimmed.match(/^([a-z-]+)\s+(?:sha256:)?[a-f0-9]{8,}$/i);
if (partStatusMatch) return {
text: `${partStatusMatch[1]} part`,
hidePercent: false
};
const hidePercent = /^verifying\b.*\bdigest\b/i.test(trimmed);
return {
text: hidePercent ? "verifying digest" : trimmed,
hidePercent
};
}
async function readOllamaPullChunkWithIdleTimeout(reader) {
return await new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reader.cancel().catch(() => void 0);
reject(/* @__PURE__ */ new Error(`Ollama pull stalled: no data received for ${Math.round(OLLAMA_PULL_STREAM_IDLE_TIMEOUT_MS / 1e3)}s`));
}, OLLAMA_PULL_STREAM_IDLE_TIMEOUT_MS);
reader.read().then(resolve, (error) => reject(toErrorObject(error, "Non-Error rejection"))).finally(() => clearTimeout(timeoutId));
});
}
async function pullOllamaModelCore(params) {
const baseUrl = resolveOllamaApiBase(params.baseUrl);
const modelName = normalizeOllamaModelName(params.modelName) ?? params.modelName.trim();
const responseController = new AbortController();
const responseTimeout = setTimeout(responseController.abort.bind(responseController), OLLAMA_PULL_RESPONSE_TIMEOUT_MS);
try {
params.signal?.throwIfAborted();
const { response, release } = await fetchWithSsrFGuard({
url: `${baseUrl}/api/pull`,
init: {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: modelName })
},
signal: params.signal ? AbortSignal.any([responseController.signal, params.signal]) : responseController.signal,
policy: buildOllamaBaseUrlSsrFPolicy(baseUrl),
auditContext: "ollama-setup.pull"
});
clearTimeout(responseTimeout);
try {
if (!response.ok) {
response.body?.cancel().catch(() => void 0);
return {
ok: false,
message: `Failed to download ${modelName} (HTTP ${response.status})`
};
}
if (!response.body) return {
ok: false,
message: `Failed to download ${modelName} (no response body)`
};
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let pendingRecordBytes = 0;
const layers = /* @__PURE__ */ new Map();
const parseLine = (line) => {
if (!line.trim()) return;
try {
const chunk = JSON.parse(line);
if (chunk.error) return {
ok: false,
message: `Download failed: ${chunk.error}`
};
if (!chunk.status || chunk.status === "success") return chunk.status ? { ok: true } : void 0;
if (chunk.total && chunk.completed !== void 0) {
layers.set(chunk.status, {
total: chunk.total,
completed: chunk.completed
});
const totals = {
total: 0,
completed: 0
};
for (const layer of layers.values()) {
totals.total += layer.total;
totals.completed += layer.completed;
}
params.onStatus?.(chunk.status, totals.total > 0 ? Math.round(totals.completed / totals.total * 100) : null);
} else params.onStatus?.(chunk.status, null);
} catch {}
};
try {
for (;;) {
const { done, value } = await readOllamaPullChunkWithIdleTimeout(reader);
if (done) {
const terminal = parseLine(buffer);
if (terminal) return terminal;
throw new Error("pull stream ended before success");
}
pendingRecordBytes = checkNdjsonRecordCap(value, pendingRecordBytes);
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
const parsed = parseLine(line);
if (parsed) return parsed;
}
}
} finally {
reader.cancel().catch(() => void 0);
reader.releaseLock();
}
} finally {
await release();
}
} catch (err) {
return {
ok: false,
message: `Failed to download ${modelName}: ${formatErrorMessage(err)}`
};
} finally {
clearTimeout(responseTimeout);
}
}
async function pullOllamaModel(baseUrl, modelName, prompter, signal) {
const spinner = prompter.progress(`Downloading ${modelName}...`);
const result = await pullOllamaModelCore({
baseUrl,
modelName,
...signal ? { signal } : {},
onStatus: (status, percent) => {
const displayStatus = formatOllamaPullStatus(status);
const progress = displayStatus.hidePercent ? "" : ` - ${percent ?? 0}%`;
spinner.update(`Downloading ${modelName} - ${displayStatus.text}${progress}`);
}
});
spinner.stop(result.ok ? `Downloaded ${modelName}` : result.message);
return result.ok;
}
async function pullOllamaModelNonInteractive(baseUrl, modelName, runtime) {
runtime.log(`Downloading ${modelName}...`);
const result = await pullOllamaModelCore({
baseUrl,
modelName
});
if (result.ok) runtime.log(`Downloaded ${modelName}`);
else runtime.error(result.message);
return result.ok;
}
//#endregion
//#region extensions/ollama/src/setup.runtime.ts
const OLLAMA_SUGGESTED_MODELS_LOCAL = [OLLAMA_DEFAULT_MODEL];
const OLLAMA_SUGGESTED_MODELS_CLOUD = OLLAMA_CLOUD_DEFAULT_MODELS.map((model) => model.id);
const OLLAMA_SUGGESTED_MODELS_LOCAL_CLOUD = OLLAMA_CLOUD_DEFAULT_MODELS.map((model) => `${model.id}:cloud`);
const OLLAMA_CLOUD_MODEL_CAP = 500;
const OLLAMA_RECOMMENDED_TOOLS_MODEL = "gemma4:e4b";
const OLLAMA_RECOMMENDED_TOOLS_MODEL_SIZE = "about 9.6 GB";
const HOST_BACKED_OLLAMA_MODE_CONFIG = {
"cloud-local": {
includeCloudModels: true,
noteTitle: "Ollama Cloud + Local"
},
"local-only": {
includeCloudModels: false,
noteTitle: "Ollama"
}
};
function buildOllamaUnreachableLines(baseUrl, retry) {
return [
`Ollama could not be reached at ${baseUrl}.`,
"Start or restart the Ollama server for this address.",
"If Ollama is not installed on that machine, download it at https://ollama.com/download",
...retry ? ["", "Continue when it is running. OpenClaw will retry this address."] : []
];
}
function buildOllamaCloudSigninLines(signinUrl) {
return [
"Cloud models on this Ollama host need `ollama signin`.",
signinUrl ?? "Run `ollama signin` on the configured Ollama host.",
"",
"Continuing with local models only for now."
];
}
async function checkOllamaCloudAuth(baseUrl) {
try {
const apiBase = resolveOllamaApiBase(baseUrl);
const { response, release } = await fetchWithSsrFGuard({
url: `${apiBase}/api/me`,
init: { method: "POST" },
timeoutMs: 5e3,
policy: buildOllamaBaseUrlSsrFPolicy(apiBase),
auditContext: "ollama-setup.me"
});
try {
if (response.status === 401) return {
signedIn: false,
signinUrl: (await readProviderJsonResponse(response, "ollama.cloud-auth")).signin_url
};
if (!response.ok) return { signedIn: false };
return { signedIn: true };
} finally {
response.body?.cancel().catch(() => void 0);
await release();
}
} catch {
return { signedIn: false };
}
}
async function promptForOllamaCloudCredential(params) {
const captured = {};
const optionToken = normalizeOptionalSecretInput(params.opts?.ollamaApiKey);
const discoveryApiKey = await ensureApiKeyFromOptionEnvOrPrompt({
token: optionToken ?? normalizeOptionalSecretInput(params.opts?.token),
tokenProvider: optionToken ? "ollama" : normalizeOptionalSecretInput(params.opts?.tokenProvider),
secretInputMode: params.allowSecretRefPrompt === false ? params.secretInputMode ?? "plaintext" : params.secretInputMode,
config: params.cfg,
env: params.env,
workspaceDir: params.workspaceDir,
expectedProviders: ["ollama"],
provider: "ollama",
envLabel: "OLLAMA_API_KEY",
promptMessage: "Ollama API key",
normalize: normalizeApiKeyInput,
validate: validateApiKeyInput,
prompter: params.prompter,
setCredential: async (apiKey, mode) => {
captured.credential = apiKey;
captured.credentialMode = mode;
}
});
if (!captured.credential) throw new Error("Missing Ollama API key input.");
if (typeof captured.credential === "string" && isNonSecretApiKeyMarker(captured.credential, { includeEnvVarName: false })) throw new Error("Cloud-only Ollama setup requires a real OLLAMA_API_KEY.");
return {
credential: captured.credential,
credentialMode: captured.credentialMode,
discoveryApiKey
};
}
function applyOllamaProviderConfig(cfg, baseUrl, modelNames, discoveredModelsByName, apiKey = OLLAMA_DEFAULT_API_KEY, defaultModels = []) {
return {
...cfg,
models: {
...cfg.models,
mode: cfg.models?.mode ?? "merge",
providers: {
...cfg.models?.providers,
ollama: {
baseUrl,
api: "ollama",
apiKey,
models: buildOllamaModelsConfig(modelNames, discoveredModelsByName, defaultModels)
}
}
}
};
}
async function promptForOllamaBaseUrl(prompter, env = process.env) {
const defaultBaseUrl = resolveOllamaSetupDefaultBaseUrl(env);
const baseUrlRaw = await prompter.text({
message: "Ollama base URL",
initialValue: defaultBaseUrl,
placeholder: defaultBaseUrl,
validate: (value) => value?.trim() ? void 0 : "Required"
});
return resolveOllamaApiBase((baseUrlRaw ?? defaultBaseUrl).trim().replace(/\/+$/, ""));
}
async function resolveHostBackedSuggestedModelNames(params) {
const modeConfig = HOST_BACKED_OLLAMA_MODE_CONFIG[params.mode];
if (!modeConfig.includeCloudModels) return OLLAMA_SUGGESTED_MODELS_LOCAL;
const auth = await checkOllamaCloudAuth(params.baseUrl);
if (auth.signedIn) return mergeUniqueModelNames(OLLAMA_SUGGESTED_MODELS_LOCAL, OLLAMA_SUGGESTED_MODELS_LOCAL_CLOUD);
await params.prompter.note(buildOllamaCloudSigninLines(auth.signinUrl).join("\n"), modeConfig.noteTitle);
return OLLAMA_SUGGESTED_MODELS_LOCAL;
}
async function promptAndConfigureHostBackedOllama(params) {
const baseUrl = await promptForOllamaBaseUrl(params.prompter, params.env);
let discovery = await discoverOllamaModelsForSetup({
baseUrl,
includeRemoteModels: HOST_BACKED_OLLAMA_MODE_CONFIG[params.mode].includeCloudModels,
inspectTools: true,
...params.signal ? { signal: params.signal } : {}
});
if (!discovery.reachable) {
await params.prompter.note(buildOllamaUnreachableLines(baseUrl, true).join("\n"), "Ollama");
if (!await params.prompter.confirm({
message: "Retry this Ollama address now?",
initialValue: true
})) throw new WizardCancelledError("Ollama setup cancelled");
params.signal?.throwIfAborted();
discovery = await discoverOllamaModelsForSetup({
baseUrl,
includeRemoteModels: HOST_BACKED_OLLAMA_MODE_CONFIG[params.mode].includeCloudModels,
inspectTools: true,
...params.signal ? { signal: params.signal } : {}
});
}
if (!discovery.reachable) throw new WizardCancelledError(`Ollama is still not reachable at ${baseUrl}`);
const { models, inspectedModels, discoveredModelsByName, inspectionFailures, hasToolsCapableModel } = discovery;
if (inspectionFailures.length > 0) await params.prompter.note([
"Some installed models could not be inspected and were skipped:",
...inspectionFailures.slice(0, 5).map((line) => `- ${line}`),
...inspectionFailures.length > 5 ? [`…and ${inspectionFailures.length - 5} more`] : []
].join("\n"), "Ollama");
let discoveredModelNames = models.map((model) => model.name);
const inspectionUsable = inspectedModels.length === 0 || inspectionFailures.length < inspectedModels.length;
if (!hasToolsCapableModel && inspectionUsable) {
if (await params.prompter.confirm({
message: `No tools-capable Ollama model is installed. Pull ${OLLAMA_RECOMMENDED_TOOLS_MODEL} (${OLLAMA_RECOMMENDED_TOOLS_MODEL_SIZE})?`,
initialValue: false
})) {
if (!await pullOllamaModel(baseUrl, OLLAMA_RECOMMENDED_TOOLS_MODEL, params.prompter, params.signal)) throw new WizardCancelledError("Failed to download recommended Ollama model");
params.signal?.throwIfAborted();
const recommendedScan = await inspectOllamaModelsForSetup(baseUrl, [{ name: OLLAMA_RECOMMENDED_TOOLS_MODEL }], params.signal);
if (recommendedScan.inspectionFailures.length > 0) throw new WizardCancelledError(`Failed to verify pulled Ollama model: ${recommendedScan.inspectionFailures[0]}`);
const [recommendedModel] = recommendedScan.inspected;
if (recommendedModel) discoveredModelsByName.set(recommendedModel.name, recommendedModel);
discoveredModelNames = mergeUniqueModelNames(discoveredModelNames, [OLLAMA_RECOMMENDED_TOOLS_MODEL]);
}
}
const suggestedModelNames = await resolveHostBackedSuggestedModelNames({
mode: params.mode,
baseUrl,
prompter: params.prompter
});
const cloudDefaultModelId = suggestedModelNames.find(isOllamaCloudModel);
const defaultModelId = selectAppGuidedOllamaModelFromDiscovery(discoveredModelsByName.values()) ?? cloudDefaultModelId;
return {
...defaultModelId ? { defaultModel: `ollama/${defaultModelId}` } : {},
config: applyOllamaProviderConfig(params.cfg, baseUrl, mergeUniqueModelNames(suggestedModelNames, discoveredModelNames), discoveredModelsByName)
};
}
async function promptAndConfigureOllama(params) {
const mode = await params.prompter.select({
message: "Ollama mode",
options: [
{
value: "cloud-local",
label: "Cloud + Local",
hint: "Route cloud and local models through your Ollama host"
},
{
value: "cloud-only",
label: "Cloud only",
hint: "Hosted Ollama models via ollama.com"
},
{
value: "local-only",
label: "Local only",
hint: "Local models only"
}
]
});
if (mode === "cloud-only") {
const { credential, credentialMode, discoveryApiKey } = await promptForOllamaCloudCredential({
cfg: params.cfg,
env: params.env,
workspaceDir: params.workspaceDir,
opts: params.opts,
prompter: params.prompter,
secretInputMode: params.secretInputMode,
allowSecretRefPrompt: params.allowSecretRefPrompt
});
const { models } = await fetchOllamaModels(OLLAMA_CLOUD_BASE_URL, {
apiKey: discoveryApiKey,
signal: params.signal
});
const discoveredModelNames = models.slice(0, OLLAMA_CLOUD_MODEL_CAP).map((model) => model.name);
const modelNames = discoveredModelNames.length > 0 ? mergeUniqueModelNames(OLLAMA_SUGGESTED_MODELS_CLOUD, discoveredModelNames) : OLLAMA_SUGGESTED_MODELS_CLOUD;
const defaultModelId = modelNames[0];
return {
credential,
credentialMode,
...defaultModelId ? { defaultModel: `ollama/${defaultModelId}` } : {},
config: applyOllamaProviderConfig(params.cfg, OLLAMA_CLOUD_BASE_URL, modelNames, void 0, credential, OLLAMA_CLOUD_DEFAULT_MODELS)
};
}
return await promptAndConfigureHostBackedOllama({
cfg: params.cfg,
mode,
prompter: params.prompter,
env: params.env,
...params.signal ? { signal: params.signal } : {}
});
}
/** Checks existing host models without pulling or mutating state before reset. */
async function validateOllamaNonInteractive(ctx) {
const configuredBaseUrl = typeof ctx.opts.customBaseUrl === "string" ? ctx.opts.customBaseUrl.trim() : void 0;
const baseUrl = resolveOllamaApiBase(configuredBaseUrl || resolveOllamaSetupDefaultBaseUrl());
const discovery = await fetchOllamaModels(baseUrl);
const fail = (message) => {
ctx.runtime.error(message);
ctx.runtime.exit(1);
return false;
};
if (!discovery.reachable) return fail(`Ollama could not be reached at ${baseUrl}.\nDownload it at https://ollama.com/download`);
const requestedModel = normalizeOllamaModelName(typeof ctx.opts.customModelId === "string" ? ctx.opts.customModelId : void 0);
const availableModelNames = discovery.models.map((model) => model.name);
if (requestedModel && isOllamaCloudModel(requestedModel)) {
const cloudAuth = await checkOllamaCloudAuth(baseUrl);
if (!cloudAuth.signedIn) return fail(`Cloud models on this Ollama host need \`ollama signin\`.\n${cloudAuth.signinUrl ?? "Run `ollama signin` on the configured Ollama host."}`);
const showInfo = await queryOllamaModelShowInfo(baseUrl, requestedModel);
if (typeof showInfo.contextWindow !== "number" && (showInfo.capabilities?.length ?? 0) === 0) return fail(`Ollama model ${requestedModel} was not found at ${baseUrl}.\nAvailable models: ${availableModelNames.join(", ") || "(none)"}`);
return true;
}
if (availableModelNames.length === 0) return fail(`No Ollama models are available at ${baseUrl}.\nPull a model first, then re-run setup.`);
if (requestedModel && !findAvailableOllamaModelName(requestedModel, availableModelNames)) return fail(`Ollama model ${requestedModel} was not found at ${baseUrl}.\nAvailable models: ${availableModelNames.join(", ")}`);
return true;
}
async function configureOllamaNonInteractive(params) {
const baseUrl = resolveOllamaApiBase((params.opts.customBaseUrl?.trim() || resolveOllamaSetupDefaultBaseUrl()).replace(/\/+$/, ""));
const { reachable, models, discoveredModelsByName } = await discoverOllamaModelsForSetup({ baseUrl });
const explicitModel = normalizeOllamaModelName(params.opts.customModelId);
if (!reachable) {
params.runtime.error(buildOllamaUnreachableLines(baseUrl, false).join("\n"));
params.runtime.exit(1);
return params.nextConfig;
}
const modelNames = models.map((model) => model.name);
const orderedModelNames = mergeUniqueModelNames(OLLAMA_SUGGESTED_MODELS_LOCAL.filter((modelName) => findAvailableOllamaModelName(modelName, modelNames) !== void 0), modelNames);
const requestedDefaultModelId = explicitModel ?? selectAppGuidedOllamaModelFromDiscovery(discoveredModelsByName.values()) ?? expectDefined(OLLAMA_SUGGESTED_MODELS_LOCAL[0], "default suggested Ollama model");
const availableModelNames = new Set(modelNames);
const availableDefaultModelId = findAvailableOllamaModelName(requestedDefaultModelId, availableModelNames);
const requestedCloudModel = isOllamaCloudModel(requestedDefaultModelId);
let pulledRequestedModel = false;
if (requestedCloudModel) availableModelNames.add(requestedDefaultModelId);
else if (!availableDefaultModelId) {
pulledRequestedModel = await pullOllamaModelNonInteractive(baseUrl, requestedDefaultModelId, params.runtime);
if (pulledRequestedModel) availableModelNames.add(requestedDefaultModelId);
}
let allModelNames = orderedModelNames;
let defaultModelId = availableDefaultModelId ?? requestedDefaultModelId;
if ((pulledRequestedModel || requestedCloudModel) && !allModelNames.includes(requestedDefaultModelId)) allModelNames = [...allModelNames, requestedDefaultModelId];
const inspectAvailableModel = async (name) => {
const model = discoveredModelsByName.get(name) ?? expectDefined((await enrichOllamaModelsWithContext(baseUrl, [models.find((candidate) => candidate.name === name) ?? { name }]))[0], "selected Ollama setup model");
discoveredModelsByName.set(name, model);
return model;
};
if (!findAvailableOllamaModelName(defaultModelId, availableModelNames)) {
let fallbackModelId;
for (const name of allModelNames) {
const availableName = findAvailableOllamaModelName(name, availableModelNames);
if (availableName && !isOllamaEmbeddingOnlyModel(await inspectAvailableModel(availableName))) {
fallbackModelId = availableName;
break;
}
}
if (!fallbackModelId) {
params.runtime.error([`No Ollama chat models are available at ${baseUrl}.`, "Pull a chat model first, then re-run setup."].join("\n"));
params.runtime.exit(1);
return params.nextConfig;
}
defaultModelId = fallbackModelId;
params.runtime.log(`Ollama model ${requestedDefaultModelId} was not available; using ${defaultModelId} instead.`);
}
if (!requestedCloudModel) await inspectAvailableModel(defaultModelId);
const config = applyOllamaProviderConfig(params.nextConfig, baseUrl, allModelNames, discoveredModelsByName);
params.runtime.log(`Default Ollama model: ${defaultModelId}`);
return applyAgentDefaultModelPrimary(config, `ollama/${defaultModelId}`);
}
async function ensureOllamaModelPulled(params) {
if (!params.model.startsWith("ollama/")) return;
const baseUrl = readProviderBaseUrl(params.config.models?.providers?.ollama) ?? "http://127.0.0.1:11434";
const modelName = params.model.slice(7);
if (isOllamaCloudModel(modelName)) return;
const { models } = await fetchOllamaModels(baseUrl);
if (findAvailableOllamaModelName(modelName, models.map((model) => model.name))) return;
if (!await pullOllamaModel(baseUrl, modelName, params.prompter)) throw new WizardCancelledError("Failed to download selected Ollama model");
}
//#endregion
export { buildOllamaProvider, checkOllamaCloudAuth, configureOllamaNonInteractive, ensureOllamaModelPulled, promptAndConfigureOllama, resolveOllamaSetupDefaultBaseUrl, validateOllamaNonInteractive };