190 lines
8.5 KiB
JavaScript
190 lines
8.5 KiB
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, endpoints, 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, undefined, endpoints);
|
|
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, endpoints);
|
|
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,
|
|
endpoints
|
|
});
|
|
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
|