node-llama-cpp
Version:
Run AI models locally on your machine with node.js bindings for llama.cpp. Enforce a JSON schema on the model output on the generation level
189 lines • 8.69 kB
JavaScript
import path from "path";
import fs from "fs-extra";
import chalk from "chalk";
import { cliModelsDirectory } from "../config.js";
import { getReadablePath } from "../cli/utils/getReadablePath.js";
import { resolveSplitGgufParts } from "../gguf/utils/resolveSplitGgufParts.js";
import { resolveModelDestination } from "./resolveModelDestination.js";
import { createModelDownloader } from "./createModelDownloader.js";
import { genericFilePartNumber } from "./parseModelUri.js";
import { isFilePartText } from "./parseModelFileName.js";
import { pushAll } from "./pushAll.js";
/**
* Resolves a local model file path from a URI or file path, and downloads the necessary files first if needed.
*
* If a URL or a URI is given, it'll be resolved to the corresponding file path.
* If the file path exists, it will be returned, otherwise it will be downloaded and then be returned.
*
* If a file path is given, and the path exists, it will be returned, otherwise an error will be thrown.
*
* Files are resolved from and downloaded to the `directory` option,
* which defaults to `node-llama-cpp`'s default global models directory (`~/.node-llama-cpp/models`).
*
* Set the `cli` option to `false` to hide the download progress from the console.
* @example
* ```typescript
* import {fileURLToPath} from "url";
* import path from "path";
* import {getLlama, resolveModelFile} from "node-llama-cpp";
*
* const __dirname = path.dirname(fileURLToPath(import.meta.url));
*
* // resolve a model from Hugging Face to the models directory
* const modelPath = await resolveModelFile(
* "hf:user/model:quant",
* path.join(__dirname, "models")
* );
*
* const llama = await getLlama();
* const model = await llama.loadModel({modelPath});
* ```
* @example
* ```typescript
* import {fileURLToPath} from "url";
* import path from "path";
* import {getLlama, resolveModelFile} from "node-llama-cpp";
*
* const __dirname = path.dirname(fileURLToPath(import.meta.url));
*
* // resolve a model from a URL to the models directory
* const modelPath = await resolveModelFile(
* "https://example.com/model.gguf",
* path.join(__dirname, "models")
* );
*
* const llama = await getLlama();
* const model = await llama.loadModel({modelPath});
* ```
* @example
* ```typescript
* import {fileURLToPath} from "url";
* import path from "path";
* import {getLlama, resolveModelFile} from "node-llama-cpp";
*
* const __dirname = path.dirname(fileURLToPath(import.meta.url));
*
* // resolve a local model that is in the models directory
* const modelPath = await resolveModelFile(
* "model.gguf",
* path.join(__dirname, "models")
* );
*
* const llama = await getLlama();
* const model = await llama.loadModel({modelPath});
* ```
* @returns The resolved model file path
*/
export async function resolveModelFile(uriOrPath, optionsOrDirectory) {
const { directory, download = "auto", verify = false, fileName, headers, cli = true, onProgress, deleteTempFileOnCancel = true, parallel = 4, tokens, signal } = typeof optionsOrDirectory === "string"
? { directory: optionsOrDirectory }
: (optionsOrDirectory ?? {});
const resolvedDirectory = directory || cliModelsDirectory;
const resolvedCli = cli == null ? true : cli;
let resolvedVerify = verify ?? false;
if (download === false)
resolvedVerify = false;
const resolvedModelDestination = resolveModelDestination(uriOrPath);
if (resolvedModelDestination.type === "file") {
const resolvedFilePath = path.resolve(resolvedDirectory, uriOrPath);
if (await fs.pathExists(resolvedFilePath))
return resolvedFilePath;
throw new Error(`No model file found at "${resolvedFilePath}"`);
}
const expectedFileNames = fileName != null
? [fileName]
: [];
if (expectedFileNames.length === 0 && resolvedModelDestination.type === "uri") {
if (resolvedModelDestination.parsedUri.type === "resolved")
expectedFileNames.push(resolvedModelDestination.parsedUri.fullFilename);
else
pushAll(expectedFileNames, resolvedModelDestination.parsedUri.possibleFullFilenames);
}
else if (expectedFileNames.length === 0 && resolvedModelDestination.type === "url") {
const enforcedParsedUrl = resolveModelDestination(uriOrPath, true);
if (enforcedParsedUrl != null && enforcedParsedUrl.type === "uri") {
if (enforcedParsedUrl.parsedUri.type === "resolved")
expectedFileNames.push(enforcedParsedUrl.parsedUri.fullFilename);
else
pushAll(expectedFileNames, enforcedParsedUrl.parsedUri.possibleFullFilenames);
}
}
const foundExpectedFilePath = await findMatchingFilesInDirectory(resolvedDirectory, expectedFileNames);
if (foundExpectedFilePath != null && !resolvedVerify) {
const allGgufParts = resolveSplitGgufParts(foundExpectedFilePath);
if (allGgufParts.length === 1 && allGgufParts[0] === foundExpectedFilePath)
return foundExpectedFilePath;
const allPartsExist = await Promise.all(allGgufParts.map((part) => fs.pathExists(part)));
if (allGgufParts.length > 0) {
if (allPartsExist.every((exists) => exists))
return allGgufParts[0];
else if (download === false)
throw new Error(`Not all split parts of the model file "${allGgufParts[0]}" are present in the same directory`);
}
}
if (download === false) {
if (expectedFileNames.length === 1)
throw new Error(`No model file found at "${path.join(resolvedDirectory, expectedFileNames[0])}" and download is disabled`);
throw new Error(`No model file found for "${uriOrPath}" at "${resolvedDirectory}" and download is disabled`);
}
if (signal?.aborted)
throw signal.reason;
const downloader = await createModelDownloader({
modelUri: resolvedModelDestination.type === "uri"
? resolvedModelDestination.uri
: resolvedModelDestination.url,
dirPath: resolvedDirectory,
headers,
showCliProgress: resolvedCli,
deleteTempFileOnCancel,
skipExisting: true,
fileName: fileName || undefined,
parallelDownloads: parallel,
onProgress,
tokens
});
if (foundExpectedFilePath != null && downloader.totalFiles === 1 && await fs.pathExists(downloader.entrypointFilePath)) {
const fileStats = await fs.stat(foundExpectedFilePath);
if (downloader.totalSize === fileStats.size) {
await downloader.cancel({ deleteTempFile: false });
return foundExpectedFilePath;
}
}
if (resolvedCli)
console.info(`Downloading to ${chalk.yellow(getReadablePath(resolvedDirectory))}${downloader.splitBinaryParts != null
? chalk.gray(` (combining ${downloader.splitBinaryParts} parts into a single file)`)
: ""}`);
await downloader.download({ signal });
if (resolvedCli)
console.info(`Downloaded to ${chalk.yellow(getReadablePath(downloader.entrypointFilePath))}`);
return downloader.entrypointFilePath;
}
async function findMatchingFilesInDirectory(dirPath, fileNames) {
let directoryFileNames = undefined;
if (!(await fs.pathExists(dirPath)) || !(await fs.stat(dirPath)).isDirectory())
return undefined;
for (const expectedFileName of fileNames) {
if (expectedFileName.includes(genericFilePartNumber)) {
const [firstPart, ...restParts] = expectedFileName.split(genericFilePartNumber);
const resolvedFirstPart = firstPart || "";
const resolvedLastParts = restParts.join(genericFilePartNumber);
if (directoryFileNames == null)
directoryFileNames = (await fs.readdir(dirPath, { withFileTypes: true }))
.filter((item) => item.isFile())
.map((item) => item.name);
for (const directoryFileName of directoryFileNames) {
if (directoryFileName.startsWith(resolvedFirstPart) && directoryFileName.endsWith(resolvedLastParts)) {
const numberPart = directoryFileName.slice(resolvedFirstPart.length, -resolvedLastParts.length);
if (isFilePartText(numberPart))
return path.join(dirPath, directoryFileName);
}
}
continue;
}
const testPath = path.join(dirPath, expectedFileName);
if (await fs.pathExists(testPath))
return testPath;
}
return undefined;
}
//# sourceMappingURL=resolveModelFile.js.map