src/domain/local-n8n-model.js
Rama main · réplica de lectura del repositorio original.
import { isDeepStrictEqual } from "node:util";
import { validateLocalDockerHost } from "../infrastructure/local-process.js";
import { validateDockerName } from "./validation.js";
import { validateDockerObjectId } from "./local-n8n-sidecar.js";
import { validateInstallId } from "./local-endpoints.js";
import { getProviderTargetBinding } from "./provider-lifecycle.js";
import { getLocalModelDefinition, LOCAL_MODEL_CATALOG_REVISION } from "../local-model/catalog.mjs";
export { getLocalModelDefinition, LOCAL_MODEL_CATALOG } from "../local-model/catalog.mjs";
export const LOCAL_N8N_MODEL_TARGET = "n8n-local-model";
export const LOCAL_N8N_MODEL_HOSTNAME = "n8n-local-model";
export const LOCAL_N8N_MODEL_PORT = 11434;
export const LOCAL_N8N_MODEL_ENDPOINT = "http://n8n-local-model:11434/v1";
export const LOCAL_MODEL_RUNTIME_IMAGE = "ollama/ollama:0.34.4@sha256:8262851b2846b87c649eddf3e76beb270c52f4d1bc94559f47efde16b0841551";
export const LOCAL_MODEL_HELPER_IMAGE = "node:24-bookworm-slim@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6";
const GIB = 1024 ** 3;
export function hasEnabledNoNewPrivileges(securityOptions) {
if (!Array.isArray(securityOptions)) return false;
let enabled = false;
for (const option of securityOptions) {
if (option === "no-new-privileges" || option === "no-new-privileges:true" || option === "no-new-privileges=true") {
enabled = true;
} else if (typeof option !== "string" || option.startsWith("no-new-privileges")) {
return false;
}
}
return enabled;
}
function resourceBudget(modelId, value) {
const model = getLocalModelDefinition(modelId);
if (!value || typeof value !== "object" || Array.isArray(value) ||
Object.keys(value).length !== 3 ||
!Number.isSafeInteger(value.memoryBytes) || value.memoryBytes <= 0 ||
!Number.isSafeInteger(value.diskAvailableBytes) || value.diskAvailableBytes <= 0 ||
!Number.isFinite(value.cpus) || value.cpus < 1 || value.cpus > 4096) {
throw new TypeError("Measured Docker memory, CPUs and available disk are required.");
}
const hostResources = { memoryBytes: value.memoryBytes, cpus: value.cpus, diskAvailableBytes: value.diskAvailableBytes };
const reservedMemoryBytes = Math.max(1.5 * GIB, Math.ceil(value.memoryBytes * 0.2)) + 256 * 1024 ** 2;
const requiredDiskBytes = model.expectedDownloadBytes * 2 + 12 * GIB;
if (model.memoryBytes + reservedMemoryBytes > value.memoryBytes) {
throw new RangeError("Docker memory is insufficient after reserving OS, n8n and acquisition headroom. Choose a smaller model.");
}
if (requiredDiskBytes > value.diskAvailableBytes) {
throw new RangeError("Docker disk is insufficient for the model, runtime image and download headroom.");
}
return {
contextTokens: model.contextTokens, memoryBytes: model.memoryBytes,
cpus: Math.max(0.5, Math.min(4, value.cpus * 0.75)),
expectedDownloadBytes: model.expectedDownloadBytes, requiredDiskBytes,
reservedMemoryBytes, hostResources,
};
}
export function createLocalN8nModelPlan({ dockerHost, n8nContainerId, n8nContainerName, dockerNetworkId, networkName, modelId, hostResources }) {
const binding = getProviderTargetBinding(LOCAL_N8N_MODEL_TARGET);
return {
kind: LOCAL_N8N_MODEL_TARGET, target: LOCAL_N8N_MODEL_TARGET,
label: binding.label, endpoint: LOCAL_N8N_MODEL_ENDPOINT, baseUrl: LOCAL_N8N_MODEL_ENDPOINT,
protocol: binding.protocol, upstreamAuth: binding.upstreamAuth,
dockerHost: validateLocalDockerHost(dockerHost),
n8nContainerId: validateDockerObjectId(n8nContainerId, "n8n container"),
n8nContainerName: validateDockerName(n8nContainerName),
dockerNetworkId: validateDockerObjectId(dockerNetworkId, "Docker network"),
networkName: validateDockerName(networkName),
managedPath: "~/.relmio/local/n8n-local-model", hostPublication: "none",
modelId: getLocalModelDefinition(modelId).id, catalogRevision: LOCAL_MODEL_CATALOG_REVISION,
approvedModelDigest: getLocalModelDefinition(modelId).manifestDigest,
runtimeImage: LOCAL_MODEL_RUNTIME_IMAGE, ...resourceBudget(modelId, hostResources),
authentication: "none", apiKeyPlaceholder: "local-only", responsesApi: false,
cloudEnabled: false, experimental: true,
};
}
export function normalizeLocalN8nModelPlan(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("The private local model plan is invalid.");
const normalized = createLocalN8nModelPlan(value);
if (Object.keys(value).some(key => !Object.hasOwn(normalized, key) && key !== "disposableHarnessWarning") ||
(Object.hasOwn(value, "disposableHarnessWarning") && typeof value.disposableHarnessWarning !== "boolean") ||
Object.entries(normalized).some(([key, expected]) => !isDeepStrictEqual(value[key], expected))) {
throw new TypeError("The private local model plan is invalid.");
}
return normalized;
}
export function createLocalN8nModelAcquisitionDockerfile({ installId }) {
const id = validateInstallId(installId);
return `FROM ${LOCAL_MODEL_HELPER_IMAGE}
LABEL io.relmio.managed="true" \\
io.relmio.target="${LOCAL_N8N_MODEL_TARGET}" \\
io.relmio.install="${id}"
WORKDIR /app
COPY --chown=node:node catalog.mjs acquisition.mjs /app/
USER node
ENTRYPOINT ["node", "/app/acquisition.mjs"]
`;
}
export function createLocalN8nModelDockerignore() {
return "**\n!Dockerfile.acquisition\n!catalog.mjs\n!acquisition.mjs\n";
}
export function createLocalN8nModelComposeFile({ installId, networkName, modelId, hostResources }) {
const id = validateInstallId(installId);
const network = validateDockerName(networkName);
const budget = resourceBudget(modelId, hostResources);
return `services:
local-model:
image: ${LOCAL_MODEL_RUNTIME_IMAGE}
restart: unless-stopped
init: true
user: "0:0"
environment:
OLLAMA_HOST: "0.0.0.0:11434"
OLLAMA_NO_CLOUD: "1"
OLLAMA_NOPRUNE: "1"
OLLAMA_MODELS: "/root/.ollama/models"
OLLAMA_CONTEXT_LENGTH: "${budget.contextTokens}"
OLLAMA_NUM_PARALLEL: "1"
OLLAMA_MAX_LOADED_MODELS: "1"
OLLAMA_MAX_QUEUE: "4"
OLLAMA_KEEP_ALIVE: "1m"
NVIDIA_VISIBLE_DEVICES: "void"
CUDA_VISIBLE_DEVICES: "-1"
volumes:
- model-cache:/root/.ollama
networks:
n8n-shared:
aliases:
- ${LOCAL_N8N_MODEL_HOSTNAME}
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
tmpfs:
- /tmp:size=256m,mode=1777,nodev,nosuid
pids_limit: 256
mem_limit: ${budget.memoryBytes}
memswap_limit: ${budget.memoryBytes}
cpus: ${budget.cpus}
healthcheck:
test: ["CMD", "/bin/ollama", "list"]
interval: 10s
timeout: 5s
retries: 9
start_period: 20s
logging:
driver: json-file
options:
max-size: "1m"
max-file: "1"
labels:
io.relmio.managed: "true"
io.relmio.target: "${LOCAL_N8N_MODEL_TARGET}"
io.relmio.install: "${id}"
acquisition:
profiles: ["acquisition"]
image: relmio-n8n-local-model-acquisition-${id}:local
build:
context: .
dockerfile: Dockerfile.acquisition
restart: "no"
init: true
user: "1000:1000"
networks:
- n8n-shared
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
pids_limit: 64
mem_limit: 268435456
memswap_limit: 268435456
cpus: 0.5
logging:
driver: json-file
options:
max-size: "1m"
max-file: "1"
labels:
io.relmio.managed: "true"
io.relmio.target: "${LOCAL_N8N_MODEL_TARGET}"
io.relmio.install: "${id}"
networks:
n8n-shared:
external: true
name: ${JSON.stringify(network)}
volumes:
model-cache:
labels:
io.relmio.managed: "true"
io.relmio.target: "${LOCAL_N8N_MODEL_TARGET}"
io.relmio.install: "${id}"
`;
}