openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
307 lines (306 loc) • 13.5 kB
JavaScript
import { s as asFiniteNumber } from "./number-coercion-CLj0HTDM.js";
import { r as asNullableRecord } from "./record-coerce-DItp3I4t.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { g as readResponseTextLimited, m as readProviderJsonResponse } from "./provider-http-errors-U-nhuk_f.js";
import { i as fetchWithSsrFGuard } from "./fetch-guard-BMdGQhbb.js";
import { t as jsonResult } from "./tool-results-BCM3fdVS.js";
import { c as readFiniteNumberParam, d as readPositiveIntegerParam, h as readToolStringParam } from "./common-Bm6UTDDA.js";
import "./error-runtime-Bz9Tw57Z.js";
import { t as expectDefined } from "./expect-runtime-CJBt0Gq2.js";
import "./string-coerce-runtime-GQa0ehRA.js";
import "./ssrf-runtime-Bum5C6NN.js";
import "./channel-actions-CrPVkBaw.js";
import "./provider-http-k9RMI7iG.js";
import "./param-readers-BElxWJzG.js";
import { o as OLLAMA_DEFAULT_BASE_URL } from "./defaults-BiE2_Zq0.js";
import { b as throwIfOllamaRequestAborted, c as enrichOllamaModelsWithContext, l as fetchLoadedOllamaModelNames, n as buildOllamaBaseUrlSsrFPolicy, p as isOllamaRemoteModel, s as enrichOllamaCompletionModels, u as fetchOllamaModels, y as resolveOllamaApiBase } from "./provider-models-BK60TdeT.js";
import { a as MAX_SYSTEM_PROMPT_CHARS, c as OLLAMA_MODELS_COMMAND, f as ollamaNodeInferenceToolDefinition, i as MAX_PROMPT_CHARS, l as OLLAMA_NODE_INFERENCE_CAPABILITY, n as DISCOVERY_TRANSPORT_TIMEOUT_MS, s as OLLAMA_CHAT_COMMAND } from "./node-inference-contract-sb8R04qK.js";
//#region extensions/ollama/src/node-inference.ts
const MAX_ERROR_BODY_BYTES = 500;
function readNodeCommandParams(paramsJSON) {
if (!paramsJSON) return {};
const parsed = asNullableRecord(JSON.parse(paramsJSON));
if (!parsed) throw new Error("node inference params must be a JSON object");
return parsed;
}
function durationMs(value) {
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return;
return Math.round(value / 1e6 * 100) / 100;
}
async function requestOllamaJson(params) {
const apiBase = resolveOllamaApiBase(params.baseUrl);
let response;
let release;
try {
const guarded = await fetchWithSsrFGuard({
url: `${apiBase}${params.path}`,
init: params.init,
timeoutMs: params.timeoutMs,
...params.signal ? { signal: params.signal } : {},
policy: buildOllamaBaseUrlSsrFPolicy(apiBase),
auditContext: `ollama-node-inference${params.path}`
});
response = guarded.response;
release = guarded.release;
} catch (error) {
throwIfOllamaRequestAborted(params.signal);
throw new Error(`Ollama is unavailable at ${apiBase}: ${formatErrorMessage(error)}`, { cause: error });
}
try {
if (!response.ok) {
const body = (await readResponseTextLimited(response, MAX_ERROR_BODY_BYTES)).trim();
let detail = body;
try {
const parsed = asNullableRecord(JSON.parse(body));
detail = typeof parsed?.error === "string" ? parsed.error : body;
} catch {}
throw new Error(`Ollama ${params.path} failed (HTTP ${response.status})${detail ? `: ${detail}` : ""}`);
}
return await readProviderJsonResponse(response, `ollama-node-inference${params.path}`);
} finally {
await release();
}
}
async function discoverOllamaNodeModels(baseUrl = OLLAMA_DEFAULT_BASE_URL, signal) {
const apiBase = resolveOllamaApiBase(baseUrl);
const discovered = await fetchOllamaModels(apiBase, signal ? { signal } : void 0);
if (!discovered.reachable) throw new Error(`Ollama is not running at ${apiBase}`);
const localModels = discovered.models.filter((model) => !isOllamaRemoteModel(model));
const loaded = await fetchLoadedOllamaModelNames(apiBase, signal ? { signal } : void 0);
const loadedNames = new Set(loaded.models);
const prioritizedModels = localModels.toSorted((left, right) => Number(loadedNames.has(right.name)) - Number(loadedNames.has(left.name)));
return {
provider: "ollama",
models: (await enrichOllamaCompletionModels(apiBase, prioritizedModels, {
requireCompletionCapability: true,
...signal ? { signal } : {}
})).map((model) => {
const details = model.details;
const row = {
name: model.name,
loaded: loadedNames.has(model.name)
};
if (typeof model.size === "number") row.size = model.size;
if (typeof model.modified_at === "string") row.modifiedAt = model.modified_at;
if (details?.family) row.family = details.family;
if (details?.parameter_size) row.parameterSize = details.parameter_size;
if (details?.quantization_level) row.quantization = details.quantization_level;
if (typeof model.contextWindow === "number") row.contextWindow = model.contextWindow;
if (model.capabilities) row.capabilities = model.capabilities;
return row;
}).toSorted((left, right) => {
if (left.loaded !== right.loaded) return left.loaded ? -1 : 1;
return (left.size ?? Number.MAX_SAFE_INTEGER) - (right.size ?? Number.MAX_SAFE_INTEGER) || left.name.localeCompare(right.name);
})
};
}
async function runOllamaNodeChat(params) {
const apiBase = resolveOllamaApiBase(params.baseUrl);
const deadlineMs = performance.now() + params.timeoutMs;
const remainingTimeoutMs = () => {
const remainingMs = Math.ceil(deadlineMs - performance.now());
if (remainingMs <= 0) throw new Error(`Ollama node inference timed out after ${params.timeoutMs}ms`);
return remainingMs;
};
const discovered = await fetchOllamaModels(apiBase, {
timeoutMs: remainingTimeoutMs(),
...params.signal ? { signal: params.signal } : {}
});
const localModel = discovered.models.find((model) => model.name === params.model && !isOllamaRemoteModel(model));
const [model] = localModel ? await enrichOllamaModelsWithContext(apiBase, [localModel], {
timeoutMs: remainingTimeoutMs(),
...params.signal ? { signal: params.signal } : {}
}) : [];
if (!discovered.reachable || model?.capabilities?.includes("completion") !== true) {
remainingTimeoutMs();
throw new Error(`Ollama model ${JSON.stringify(params.model)} is not a local chat model; discover models first`);
}
const messages = [...params.system ? [{
role: "system",
content: params.system
}] : [], {
role: "user",
content: params.prompt
}];
const data = await requestOllamaJson({
baseUrl: params.baseUrl,
path: "/api/chat",
timeoutMs: remainingTimeoutMs(),
...params.signal ? { signal: params.signal } : {},
init: {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: params.model,
messages,
stream: false,
think: false,
options: {
num_predict: params.maxTokens,
...params.temperature !== void 0 && { temperature: params.temperature }
}
})
}
});
const response = typeof data.message?.content === "string" ? data.message.content : void 0;
if (response === void 0) throw new Error("Ollama /api/chat response did not contain message.content");
if (data.done_reason === "length") throw new Error(`Ollama stopped after reaching maxTokens (${params.maxTokens}); retry with a larger maxTokens value`);
const promptTokens = asFiniteNumber(data.prompt_eval_count);
const completionTokens = asFiniteNumber(data.eval_count);
const loadMs = durationMs(data.load_duration);
const totalMs = durationMs(data.total_duration);
return {
provider: "ollama",
model: typeof data.model === "string" && data.model.trim() ? data.model : params.model,
response,
...promptTokens !== void 0 || completionTokens !== void 0 ? { usage: {
promptTokens,
completionTokens
} } : {},
...loadMs !== void 0 || totalMs !== void 0 ? { timings: {
loadMs,
totalMs
} } : {}
};
}
function createOllamaNodeHostCommands(options) {
const baseUrl = options?.baseUrl ?? "http://127.0.0.1:11434";
return [{
command: OLLAMA_MODELS_COMMAND,
cap: OLLAMA_NODE_INFERENCE_CAPABILITY,
handle: async (_paramsJSON, _io, context) => JSON.stringify(await discoverOllamaNodeModels(baseUrl, context?.signal))
}, {
command: OLLAMA_CHAT_COMMAND,
cap: OLLAMA_NODE_INFERENCE_CAPABILITY,
handle: async (paramsJSON, _io, context) => {
const params = readNodeCommandParams(paramsJSON);
const model = readToolStringParam(params, "model", { required: true });
const prompt = readToolStringParam(params, "prompt", {
required: true,
trim: false
});
const system = readToolStringParam(params, "system", { trim: false });
const maxTokens = readPositiveIntegerParam(params, "maxTokens", {
max: 8192,
message: `maxTokens must be an integer between 1 and 8192`
}) ?? 512;
const timeoutMs = readPositiveIntegerParam(params, "timeoutMs", {
max: 6e5,
message: `timeoutMs must be an integer between 1 and 600000`
}) ?? 12e4;
const temperature = readFiniteNumberParam(params, "temperature", {
min: 0,
max: 2,
message: "temperature must be between 0 and 2"
});
if (prompt.length > 128e3) throw new Error(`prompt exceeds ${MAX_PROMPT_CHARS} characters`);
if (system && system.length > 32e3) throw new Error(`system exceeds ${MAX_SYSTEM_PROMPT_CHARS} characters`);
return JSON.stringify(await runOllamaNodeChat({
baseUrl,
model,
prompt,
system,
temperature,
maxTokens,
timeoutMs,
...context?.signal ? { signal: context.signal } : {}
}));
}
}];
}
function findNode(nodes, query) {
const normalized = query.trim().toLowerCase();
const matches = nodes.filter((node) => node.nodeId.toLowerCase() === normalized || node.displayName?.toLowerCase() === normalized);
if (matches.length === 0) throw new Error(`node ${JSON.stringify(query)} is not connected with Ollama inference support`);
if (matches.length > 1) throw new Error(`node ${JSON.stringify(query)} is ambiguous; use its nodeId`);
return expectDefined(matches[0], "single matching Ollama inference node");
}
function parseInvokePayload(raw) {
const result = asNullableRecord(raw);
let payload = asNullableRecord(result?.payload);
if (!payload && typeof result?.payloadJSON === "string") payload = asNullableRecord(JSON.parse(result.payloadJSON));
if (!payload) throw new Error("node returned an invalid Ollama inference payload");
return payload;
}
async function invokeNode(api, nodeId, command, params, timeoutMs, signal) {
throwIfOllamaRequestAborted(signal);
return parseInvokePayload(await api.runtime.nodes.invoke({
nodeId,
command,
params,
timeoutMs,
scopes: ["operator.write"],
...signal ? { signal } : {}
}));
}
function createOllamaNodeInferenceTool(api) {
return {
...ollamaNodeInferenceToolDefinition,
execute: async (_toolCallId, args, signal) => {
throwIfOllamaRequestAborted(signal);
const params = asNullableRecord(args) ?? {};
const action = readToolStringParam(params, "action", { required: true });
const nodeQuery = readToolStringParam(params, "node");
const modelNodes = (await api.runtime.nodes.list({ connected: true })).nodes.filter((node) => (node.invocableCommands ?? node.commands)?.includes(OLLAMA_MODELS_COMMAND));
if (action === "discover") {
const targets = nodeQuery ? [findNode(modelNodes, nodeQuery)] : modelNodes;
const nodes = await Promise.all(targets.map(async (node) => {
try {
const payload = await invokeNode(api, node.nodeId, OLLAMA_MODELS_COMMAND, {}, DISCOVERY_TRANSPORT_TIMEOUT_MS, signal);
const result = {
nodeId: node.nodeId,
ok: true
};
if (node.displayName) result.displayName = node.displayName;
return Object.assign(result, payload);
} catch (error) {
throwIfOllamaRequestAborted(signal);
const result = {
nodeId: node.nodeId,
ok: false,
error: formatErrorMessage(error)
};
if (node.displayName) result.displayName = node.displayName;
return result;
}
}));
return jsonResult({
nodes,
...modelNodes.length === 0 && { hint: "No connected node advertises Ollama inference. Start Ollama and `openclaw node run` on the target machine, then approve any request shown by `openclaw nodes pending`." }
});
}
if (action !== "run") throw new Error("action must be discover or run");
const chatNodes = modelNodes.filter((node) => (node.invocableCommands ?? node.commands)?.includes(OLLAMA_CHAT_COMMAND));
const node = nodeQuery ? findNode(chatNodes, nodeQuery) : chatNodes.length === 1 ? chatNodes[0] : void 0;
if (!node) throw new Error(chatNodes.length === 0 ? "no connected node advertises Ollama inference" : "multiple nodes advertise Ollama inference; specify node");
const model = readToolStringParam(params, "model", { required: true });
const prompt = readToolStringParam(params, "prompt", {
required: true,
trim: false
});
const maxTokens = readPositiveIntegerParam(params, "maxTokens", { max: 8192 }) ?? 512;
const timeoutMs = readPositiveIntegerParam(params, "timeoutMs", { max: 6e5 }) ?? 12e4;
const system = readToolStringParam(params, "system", { trim: false });
const temperature = readFiniteNumberParam(params, "temperature", {
min: 0,
max: 2
});
const commandParams = {
model,
prompt,
maxTokens,
timeoutMs
};
if (system !== void 0) commandParams.system = system;
if (temperature !== void 0) commandParams.temperature = temperature;
const result = await invokeNode(api, node.nodeId, OLLAMA_CHAT_COMMAND, commandParams, timeoutMs, signal);
return jsonResult({
nodeId: node.nodeId,
...node.displayName && { displayName: node.displayName },
...result
});
}
};
}
//#endregion
export { createOllamaNodeHostCommands, createOllamaNodeInferenceTool };