Skip to content
This repository was archived by the owner on Jul 4, 2025. It is now read-only.

Commit 91e2c24

Browse files
committed
refactor(nitro-node): move checkMagicBytes functions out to utils
1 parent b2b749d commit 91e2c24

File tree

7 files changed

+74
-67
lines changed

7 files changed

+74
-67
lines changed

nitro-node/jest.config.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import type { JestConfigWithTsJest } from 'ts-jest'
1+
import type { JestConfigWithTsJest } from "ts-jest";
22

33
const jestConfig: JestConfigWithTsJest = {
4-
preset: 'ts-jest',
5-
testEnvironment: 'node',
6-
transformIgnorePatterns: ['/node_modules/']
7-
}
4+
preset: "ts-jest",
5+
testEnvironment: "node",
6+
transformIgnorePatterns: ["/node_modules/"],
7+
globals: {
8+
RELEASE_URL_PREFIX: "https://api.github.com/repos/janhq/nitro/releases/",
9+
TAGGED_RELEASE_URL_PREFIX:
10+
"https://api.github.com/repos/janhq/nitro/releases/tags",
11+
},
12+
};
813

9-
export default jestConfig
14+
export default jestConfig;

nitro-node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./types";
22
export * from "./nitro";
3+
export { setLogger } from "./logger";

nitro-node/src/logger.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import os from "node:os";
2+
import { NitroLogger } from "./types";
3+
4+
// The logger to use, default to stdout
5+
export let log: NitroLogger = (message, ..._) =>
6+
process.stdout.write(message + os.EOL);
7+
8+
/**
9+
* Set logger before running nitro
10+
* @param {NitroLogger} logger The logger to use
11+
*/
12+
export async function setLogger(logger: NitroLogger): Promise<void> {
13+
log = logger;
14+
}

nitro-node/src/nitro.ts

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import os from "node:os";
21
import fs from "node:fs";
32
import path from "node:path";
43
import { ChildProcessWithoutNullStreams, spawn } from "node:child_process";
@@ -14,12 +13,13 @@ import {
1413
NitroNvidiaConfig,
1514
NitroModelSetting,
1615
NitroPromptSetting,
17-
NitroLogger,
1816
NitroModelOperationResponse,
1917
NitroModelInitOptions,
2018
ResourcesInfo,
2119
} from "./types";
2220
import { downloadNitro } from "./scripts";
21+
import { checkMagicBytes } from "./utils";
22+
import { log } from "./logger";
2323
// Polyfill fetch with retry
2424
const fetchRetry = fetchRT(fetch);
2525

@@ -58,7 +58,7 @@ const NVIDIA_DEFAULT_CONFIG: NitroNvidiaConfig = {
5858
const SUPPORTED_MODEL_FORMATS = [".gguf"];
5959

6060
// The supported model magic number
61-
const SUPPORTED_MODEL_MAGIC_NUMBERS = ["GGUF"];
61+
const SUPPORTED_MODEL_MAGIC_BYTES = ["GGUF"];
6262

6363
// The subprocess instance for Nitro
6464
let subprocess: ChildProcessWithoutNullStreams | undefined = undefined;
@@ -68,9 +68,6 @@ let currentModelFile: string = "";
6868
let currentSettings: NitroModelSetting | undefined = undefined;
6969
// The Nvidia info file for checking for CUDA support on the system
7070
let nvidiaConfig: NitroNvidiaConfig = NVIDIA_DEFAULT_CONFIG;
71-
// The logger to use, default to stdout
72-
let log: NitroLogger = (message, ..._) =>
73-
process.stdout.write(message + os.EOL);
7471
// The absolute path to bin directory
7572
let binPath: string = path.join(__dirname, "..", "bin");
7673

@@ -118,14 +115,6 @@ export async function setNvidiaConfig(
118115
nvidiaConfig = config;
119116
}
120117

121-
/**
122-
* Set logger before running nitro
123-
* @param {NitroLogger} logger The logger to use
124-
*/
125-
export async function setLogger(logger: NitroLogger): Promise<void> {
126-
log = logger;
127-
}
128-
129118
/**
130119
* Stops a Nitro subprocess.
131120
* @param wrapper - The model wrapper.
@@ -135,55 +124,32 @@ export function stopModel(): Promise<NitroModelOperationResponse> {
135124
return killSubprocess();
136125
}
137126

138-
/**
139-
* Read the magic bytes from a file and check if they match the provided magic bytes
140-
*/
141-
export async function checkMagicBytes(
142-
filePath: string,
143-
magicBytes: string,
144-
): Promise<boolean> {
145-
const desired = Buffer.from(magicBytes);
146-
const nBytes = desired.byteLength;
147-
const chunks = [];
148-
for await (let chunk of fs.createReadStream(filePath, {
149-
start: 0,
150-
end: nBytes - 1,
151-
})) {
152-
chunks.push(chunk);
153-
}
154-
const actual = Buffer.concat(chunks);
155-
log(
156-
`Comparing file's magic bytes <${actual.toString()}> and desired <${desired.toString()}>`,
157-
);
158-
return Buffer.compare(actual, desired) === 0;
159-
}
160-
161127
/**
162128
* Initializes a Nitro subprocess to load a machine learning model.
163129
* @param modelFullPath - The absolute full path to model directory.
164130
* @param promptTemplate - The template to use for generating prompts.
165131
* @returns A Promise that resolves when the model is loaded successfully, or rejects with an error message if the model is not found or fails to load.
166132
*/
167133
export async function runModel({
168-
modelFullPath,
134+
modelPath,
169135
promptTemplate,
170136
}: NitroModelInitOptions): Promise<NitroModelOperationResponse> {
171137
// Download nitro binaries if it's not already downloaded
172138
await downloadNitro(binPath);
173-
const files: string[] = fs.readdirSync(modelFullPath);
139+
const files: string[] = fs.readdirSync(modelPath);
174140

175141
// Look for model file with supported format
176142
let ggufBinFile = files.find(
177143
(file) =>
178-
file === path.basename(modelFullPath) ||
144+
file === path.basename(modelPath) ||
179145
SUPPORTED_MODEL_FORMATS.some((ext) => file.toLowerCase().endsWith(ext)),
180146
);
181147

182148
// If not found from path and extension, try from magic number
183149
if (!ggufBinFile) {
184150
for (const f of files) {
185-
for (const magicNum of SUPPORTED_MODEL_MAGIC_NUMBERS) {
186-
if (await checkMagicBytes(path.join(modelFullPath, f), magicNum)) {
151+
for (const magicBytes of SUPPORTED_MODEL_MAGIC_BYTES) {
152+
if (await checkMagicBytes(path.join(modelPath, f), magicBytes)) {
187153
ggufBinFile = f;
188154
break;
189155
}
@@ -194,7 +160,7 @@ export async function runModel({
194160

195161
if (!ggufBinFile) throw new Error("No GGUF model file found");
196162

197-
currentModelFile = path.join(modelFullPath, ggufBinFile);
163+
currentModelFile = path.join(modelPath, ggufBinFile);
198164

199165
const nitroResourceProbe = await getResourcesInfo();
200166
// Convert promptTemplate to system_prompt, user_prompt, ai_prompt

nitro-node/src/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface NitroModelSetting extends NitroPromptSetting {
3333
* The response object for model init operation.
3434
*/
3535
export interface NitroModelInitOptions {
36-
modelFullPath: string;
36+
modelPath: string;
3737
promptTemplate?: string;
3838
}
3939

nitro-node/src/utils/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import fs from "node:fs";
2+
import { log } from "../logger";
3+
4+
/**
5+
* Read the magic bytes from a file and check if they match the provided magic bytes
6+
*/
7+
export async function checkMagicBytes(
8+
filePath: string,
9+
magicBytes: string,
10+
): Promise<boolean> {
11+
const desired = Buffer.from(magicBytes);
12+
const nBytes = desired.byteLength;
13+
const chunks = [];
14+
for await (let chunk of fs.createReadStream(filePath, {
15+
start: 0,
16+
end: nBytes - 1,
17+
})) {
18+
chunks.push(chunk);
19+
}
20+
const actual = Buffer.concat(chunks);
21+
log(
22+
`Comparing file's magic bytes <${actual.toString()}> and desired <${desired.toString()}>`,
23+
);
24+
return Buffer.compare(actual, desired) === 0;
25+
}

nitro-node/test/nitro-process.test.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import {
1414
loadLLMModel,
1515
validateModelStatus,
1616
chatCompletion,
17-
checkMagicBytes,
1817
} from "../src";
18+
import { checkMagicBytes } from "../src/utils";
1919

2020
// FIXME: Shorthand only possible for es6 targets and up
2121
//import * as model from './model.json' assert {type: 'json'}
@@ -103,24 +103,22 @@ const sleep = async (ms: number): Promise<NodeJS.Timeout> =>
103103
*/
104104
describe("Manage nitro process", () => {
105105
/// BEGIN SUITE CONFIG
106-
const modelFullPath = fs.mkdtempSync(
107-
path.join(os.tmpdir(), "nitro-node-test"),
108-
);
106+
const modelPath = fs.mkdtempSync(path.join(os.tmpdir(), "nitro-node-test"));
109107
let modelCfg: Record<string, any> = {};
110108

111109
// Setup steps before running the suite
112110
const setupHooks = [
113111
// Get model config from json
114112
getModelConfigHook((cfg) => Object.assign(modelCfg, cfg)),
115113
// Download model before starting tests
116-
downloadModelHook(modelCfg, modelFullPath),
114+
downloadModelHook(modelCfg, modelPath),
117115
];
118116
// Teardown steps after running the suite
119117
const teardownHooks = [
120118
// Stop nitro after running, regardless of error or not
121119
() => stopModel(),
122120
// On teardown, cleanup tmp directory that was created earlier
123-
cleanupTargetDirHook(modelFullPath),
121+
cleanupTargetDirHook(modelPath),
124122
];
125123
/// END SUITE CONFIG
126124

@@ -145,7 +143,7 @@ describe("Manage nitro process", () => {
145143
async () => {
146144
// Start nitro
147145
await runModel({
148-
modelFullPath,
146+
modelPath,
149147
promptTemplate: modelCfg.settings.prompt_template,
150148
});
151149
// Wait 5s for nitro to start
@@ -161,14 +159,14 @@ describe("Manage nitro process", () => {
161159
async () => {
162160
// Start nitro
163161
await runModel({
164-
modelFullPath,
162+
modelPath,
165163
promptTemplate: modelCfg.settings.prompt_template,
166164
});
167165
// Wait 5s for nitro to start
168166
await sleep(5 * 1000);
169167
// Load LLM model
170168
await loadLLMModel({
171-
llama_model_path: modelFullPath,
169+
llama_model_path: modelPath,
172170
ctx_len: modelCfg.settings.ctx_len,
173171
ngl: modelCfg.settings.ngl,
174172
embedding: false,
@@ -240,27 +238,25 @@ describe("Manage nitro process", () => {
240238
const fileName = modelCfg.source_url.split("/")?.pop() ?? "model.gguf";
241239
// Rename the extension of model file
242240
fs.renameSync(
243-
path.join(modelFullPath, fileName),
244-
path.join(modelFullPath, `${fileName.replace(/\.gguf$/gi, ".bak")}`),
241+
path.join(modelPath, fileName),
242+
path.join(modelPath, `${fileName.replace(/\.gguf$/gi, ".bak")}`),
245243
);
246244
});
247245
afterEach(async () => {
248246
const fileName = modelCfg.source_url.split("/")?.pop() ?? "model.gguf";
249247
// Restore the extension of model file
250248
fs.renameSync(
251-
path.join(modelFullPath, `${fileName.replace(/\.gguf$/gi, ".bak")}`),
252-
path.join(modelFullPath, fileName),
249+
path.join(modelPath, `${fileName.replace(/\.gguf$/gi, ".bak")}`),
250+
path.join(modelPath, fileName),
253251
);
254252
});
255253
test(
256254
"should be able to detect model file by magic number",
257255
async () => {
258-
const files = fs.readdirSync(modelFullPath) as string[];
256+
const files = fs.readdirSync(modelPath) as string[];
259257
// Test checking magic bytes
260258
const res = await Promise.all(
261-
files.map((f) =>
262-
checkMagicBytes(path.join(modelFullPath, f), "GGUF"),
263-
),
259+
files.map((f) => checkMagicBytes(path.join(modelPath, f), "GGUF")),
264260
);
265261
expect(res).toContain(true);
266262
},

0 commit comments

Comments
 (0)