mirror of https://github.com/nomic-ai/gpt4all
* feat(typescript)/dynamic template (#1287) * remove packaged yarn * prompt templates update wip * prompt template update * system prompt template, update types, remove embed promises, cleanup * support both snakecased and camelcased prompt context * fix #1277 libbert, libfalcon and libreplit libs not being moved into the right folder after build * added support for modelConfigFile param, allowing the user to specify a local file instead of downloading the remote models.json. added a warning message if code fails to load a model config. included prompt context docs by amogus. * snakecase warning, put logic for loading local models.json into listModels, added constant for the default remote model list url, test improvements, simpler hasOwnProperty call * add DEFAULT_PROMPT_CONTEXT, export new constants * add md5sum testcase and fix constants export * update types * throw if attempting to list models without a source * rebuild docs * fix download logging undefined url, toFixed typo, pass config filesize in for future progress report * added overload with union types * bump to 2.2.0, remove alpha * code speling --------- Co-authored-by: Andreas Obersteiner <8959303+iimez@users.noreply.github.com>pull/1341/head
parent
4d855afe97
commit
4e55940edf
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
yarnPath: .yarn/releases/yarn-3.6.1.cjs
|
@ -0,0 +1,38 @@
|
||||
const { normalizePromptContext, warnOnSnakeCaseKeys } = require('./util');
|
||||
|
||||
class InferenceModel {
|
||||
llm;
|
||||
config;
|
||||
|
||||
constructor(llmodel, config) {
|
||||
this.llm = llmodel;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async generate(prompt, promptContext) {
|
||||
warnOnSnakeCaseKeys(promptContext);
|
||||
const normalizedPromptContext = normalizePromptContext(promptContext);
|
||||
const result = this.llm.raw_prompt(prompt, normalizedPromptContext, () => {});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class EmbeddingModel {
|
||||
llm;
|
||||
config;
|
||||
|
||||
constructor(llmodel, config) {
|
||||
this.llm = llmodel;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
embed(text) {
|
||||
return this.llm.embed(text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
InferenceModel,
|
||||
EmbeddingModel,
|
||||
};
|
@ -1,79 +1,228 @@
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { LLModel } = require('node-gyp-build')(path.resolve(__dirname, '..'));
|
||||
const path = require("node:path");
|
||||
const os = require("node:os");
|
||||
const fsp = require("node:fs/promises");
|
||||
const { LLModel } = require("node-gyp-build")(path.resolve(__dirname, ".."));
|
||||
const {
|
||||
listModels,
|
||||
downloadModel,
|
||||
appendBinSuffixIfMissing,
|
||||
} = require('../src/util.js');
|
||||
listModels,
|
||||
downloadModel,
|
||||
appendBinSuffixIfMissing,
|
||||
normalizePromptContext,
|
||||
} = require("../src/util.js");
|
||||
const {
|
||||
DEFAULT_DIRECTORY,
|
||||
DEFAULT_LIBRARIES_DIRECTORY,
|
||||
} = require('../src/config.js');
|
||||
DEFAULT_DIRECTORY,
|
||||
DEFAULT_LIBRARIES_DIRECTORY,
|
||||
DEFAULT_MODEL_LIST_URL,
|
||||
} = require("../src/config.js");
|
||||
const {
|
||||
loadModel,
|
||||
createPrompt,
|
||||
createCompletion,
|
||||
} = require('../src/gpt4all.js');
|
||||
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve([{}, {}, {}]),
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('../src/util.js', () => {
|
||||
const actualModule = jest.requireActual('../src/util.js');
|
||||
return {
|
||||
...actualModule,
|
||||
downloadModel: jest.fn(() =>
|
||||
({ cancel: jest.fn(), promise: jest.fn() })
|
||||
)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
downloadModel.mockClear()
|
||||
});
|
||||
loadModel,
|
||||
createPrompt,
|
||||
createCompletion,
|
||||
} = require("../src/gpt4all.js");
|
||||
const { mock } = require("node:test");
|
||||
|
||||
afterEach( () => {
|
||||
fetch.mockClear();
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('utils', () => {
|
||||
test("appendBinSuffixIfMissing", () => {
|
||||
expect(appendBinSuffixIfMissing("filename")).toBe("filename.bin")
|
||||
expect(appendBinSuffixIfMissing("filename.bin")).toBe("filename.bin")
|
||||
})
|
||||
test("default paths", () => {
|
||||
expect(DEFAULT_DIRECTORY).toBe(path.resolve(os.homedir(), ".cache/gpt4all"))
|
||||
describe("config", () => {
|
||||
test("default paths constants are available and correct", () => {
|
||||
expect(DEFAULT_DIRECTORY).toBe(
|
||||
path.resolve(os.homedir(), ".cache/gpt4all")
|
||||
);
|
||||
const paths = [
|
||||
path.join(DEFAULT_DIRECTORY, "libraries"),
|
||||
path.resolve("./libraries"),
|
||||
path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
`runtimes/${process.platform}-${process.arch}/native`
|
||||
__dirname,
|
||||
"..",
|
||||
`runtimes/${process.platform}-${process.arch}/native`
|
||||
),
|
||||
process.cwd(),
|
||||
];
|
||||
expect(typeof DEFAULT_LIBRARIES_DIRECTORY).toBe('string')
|
||||
expect(DEFAULT_LIBRARIES_DIRECTORY).toBe(paths.join(';'))
|
||||
})
|
||||
|
||||
test("listModels", async () => {
|
||||
try {
|
||||
await listModels();
|
||||
} catch(e) {}
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1)
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"https://gpt4all.io/models/models.json"
|
||||
expect(typeof DEFAULT_LIBRARIES_DIRECTORY).toBe("string");
|
||||
expect(DEFAULT_LIBRARIES_DIRECTORY).toBe(paths.join(";"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("listModels", () => {
|
||||
const fakeModels = require("./models.json");
|
||||
const fakeModel = fakeModels[0];
|
||||
const mockResponse = JSON.stringify([fakeModel]);
|
||||
|
||||
let mockFetch, originalFetch;
|
||||
|
||||
beforeAll(() => {
|
||||
// Mock the fetch function for all tests
|
||||
mockFetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => JSON.parse(mockResponse),
|
||||
});
|
||||
originalFetch = global.fetch;
|
||||
global.fetch = mockFetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Reset the fetch counter after each test
|
||||
mockFetch.mockClear();
|
||||
});
|
||||
afterAll(() => {
|
||||
// Restore fetch
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("should load the model list from remote when called without args", async () => {
|
||||
const models = await listModels();
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetch).toHaveBeenCalledWith(DEFAULT_MODEL_LIST_URL);
|
||||
expect(models[0]).toEqual(fakeModel);
|
||||
});
|
||||
|
||||
it("should load the model list from a local file, if specified", async () => {
|
||||
const models = await listModels({
|
||||
file: path.resolve(__dirname, "models.json"),
|
||||
});
|
||||
expect(fetch).toHaveBeenCalledTimes(0);
|
||||
expect(models[0]).toEqual(fakeModel);
|
||||
});
|
||||
|
||||
it("should throw an error if neither url nor file is specified", async () => {
|
||||
await expect(listModels(null)).rejects.toThrow(
|
||||
"No model list source specified. Please specify either a url or a file."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("appendBinSuffixIfMissing", () => {
|
||||
it("should make sure the suffix is there", () => {
|
||||
expect(appendBinSuffixIfMissing("filename")).toBe("filename.bin");
|
||||
expect(appendBinSuffixIfMissing("filename.bin")).toBe("filename.bin");
|
||||
});
|
||||
});
|
||||
|
||||
describe("downloadModel", () => {
|
||||
let mockAbortController, mockFetch;
|
||||
const fakeModelName = "fake-model";
|
||||
|
||||
const createMockFetch = () => {
|
||||
const mockData = new Uint8Array([1, 2, 3, 4]);
|
||||
const mockResponse = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(mockData);
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const mockFetchImplementation = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
body: mockResponse,
|
||||
})
|
||||
);
|
||||
return mockFetchImplementation;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Mocking the AbortController constructor
|
||||
mockAbortController = jest.fn();
|
||||
global.AbortController = mockAbortController;
|
||||
mockAbortController.mockReturnValue({
|
||||
signal: "signal",
|
||||
abort: jest.fn(),
|
||||
});
|
||||
mockFetch = createMockFetch();
|
||||
jest.spyOn(global, "fetch").mockImplementation(mockFetch);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up mocks
|
||||
mockAbortController.mockReset();
|
||||
mockFetch.mockClear();
|
||||
global.fetch.mockRestore();
|
||||
});
|
||||
|
||||
test("should successfully download a model file", async () => {
|
||||
const downloadController = downloadModel(fakeModelName);
|
||||
const modelFilePath = await downloadController.promise;
|
||||
expect(modelFilePath).toBe(`${DEFAULT_DIRECTORY}/${fakeModelName}.bin`);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://gpt4all.io/models/fake-model.bin",
|
||||
{
|
||||
signal: "signal",
|
||||
headers: {
|
||||
"Accept-Ranges": "arraybuffer",
|
||||
"Response-Type": "arraybuffer",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
// final model file should be present
|
||||
expect(fsp.access(modelFilePath)).resolves.not.toThrow();
|
||||
|
||||
// remove the testing model file
|
||||
await fsp.unlink(modelFilePath);
|
||||
});
|
||||
|
||||
test("should error and cleanup if md5sum is not matching", async () => {
|
||||
const downloadController = downloadModel(fakeModelName, {
|
||||
md5sum: "wrong-md5sum",
|
||||
});
|
||||
// the promise should reject with a mismatch
|
||||
await expect(downloadController.promise).rejects.toThrow(
|
||||
`Model "${fakeModelName}" failed verification: Hashes mismatch.`
|
||||
);
|
||||
// fetch should have been called
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
// the file should be missing
|
||||
expect(
|
||||
fsp.access(`${DEFAULT_DIRECTORY}/${fakeModelName}.bin`)
|
||||
).rejects.toThrow();
|
||||
// partial file should also be missing
|
||||
expect(
|
||||
fsp.access(`${DEFAULT_DIRECTORY}/${fakeModelName}.part`)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
// TODO
|
||||
// test("should be able to cancel and resume a download", async () => {
|
||||
// });
|
||||
});
|
||||
|
||||
describe("normalizePromptContext", () => {
|
||||
it("should convert a dict with camelCased keys to snake_case", () => {
|
||||
const camelCased = {
|
||||
topK: 20,
|
||||
repeatLastN: 10,
|
||||
};
|
||||
|
||||
const expectedSnakeCased = {
|
||||
top_k: 20,
|
||||
repeat_last_n: 10,
|
||||
};
|
||||
|
||||
const result = normalizePromptContext(camelCased);
|
||||
expect(result).toEqual(expectedSnakeCased);
|
||||
});
|
||||
|
||||
it("should convert a mixed case dict to snake_case, last value taking precedence", () => {
|
||||
const mixedCased = {
|
||||
topK: 20,
|
||||
top_k: 10,
|
||||
repeatLastN: 10,
|
||||
};
|
||||
|
||||
const expectedSnakeCased = {
|
||||
top_k: 10,
|
||||
repeat_last_n: 10,
|
||||
};
|
||||
|
||||
const result = normalizePromptContext(mixedCased);
|
||||
expect(result).toEqual(expectedSnakeCased);
|
||||
});
|
||||
|
||||
it("should not modify already snake cased dict", () => {
|
||||
const snakeCased = {
|
||||
top_k: 10,
|
||||
repeast_last_n: 10,
|
||||
};
|
||||
|
||||
const result = normalizePromptContext(snakeCased);
|
||||
expect(result).toEqual(snakeCased);
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"order": "a",
|
||||
"md5sum": "08d6c05a21512a79a1dfeb9d2a8f262f",
|
||||
"name": "Not a real model",
|
||||
"filename": "fake-model.bin",
|
||||
"filesize": "4",
|
||||
"systemPrompt": " "
|
||||
}
|
||||
]
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue