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 path = require("node:path");
|
||||||
const os = require('node:os');
|
const os = require("node:os");
|
||||||
const { LLModel } = require('node-gyp-build')(path.resolve(__dirname, '..'));
|
const fsp = require("node:fs/promises");
|
||||||
|
const { LLModel } = require("node-gyp-build")(path.resolve(__dirname, ".."));
|
||||||
const {
|
const {
|
||||||
listModels,
|
listModels,
|
||||||
downloadModel,
|
downloadModel,
|
||||||
appendBinSuffixIfMissing,
|
appendBinSuffixIfMissing,
|
||||||
} = require('../src/util.js');
|
normalizePromptContext,
|
||||||
|
} = require("../src/util.js");
|
||||||
const {
|
const {
|
||||||
DEFAULT_DIRECTORY,
|
DEFAULT_DIRECTORY,
|
||||||
DEFAULT_LIBRARIES_DIRECTORY,
|
DEFAULT_LIBRARIES_DIRECTORY,
|
||||||
} = require('../src/config.js');
|
DEFAULT_MODEL_LIST_URL,
|
||||||
|
} = require("../src/config.js");
|
||||||
const {
|
const {
|
||||||
loadModel,
|
loadModel,
|
||||||
createPrompt,
|
createPrompt,
|
||||||
createCompletion,
|
createCompletion,
|
||||||
} = require('../src/gpt4all.js');
|
} = require("../src/gpt4all.js");
|
||||||
|
const { mock } = require("node:test");
|
||||||
|
|
||||||
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()
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach( () => {
|
describe("config", () => {
|
||||||
fetch.mockClear();
|
test("default paths constants are available and correct", () => {
|
||||||
jest.clearAllMocks()
|
expect(DEFAULT_DIRECTORY).toBe(
|
||||||
})
|
path.resolve(os.homedir(), ".cache/gpt4all")
|
||||||
|
);
|
||||||
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"))
|
|
||||||
const paths = [
|
const paths = [
|
||||||
path.join(DEFAULT_DIRECTORY, "libraries"),
|
path.join(DEFAULT_DIRECTORY, "libraries"),
|
||||||
path.resolve("./libraries"),
|
path.resolve("./libraries"),
|
||||||
path.resolve(
|
path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
"..",
|
"..",
|
||||||
`runtimes/${process.platform}-${process.arch}/native`
|
`runtimes/${process.platform}-${process.arch}/native`
|
||||||
),
|
),
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
];
|
];
|
||||||
expect(typeof DEFAULT_LIBRARIES_DIRECTORY).toBe('string')
|
expect(typeof DEFAULT_LIBRARIES_DIRECTORY).toBe("string");
|
||||||
expect(DEFAULT_LIBRARIES_DIRECTORY).toBe(paths.join(';'))
|
expect(DEFAULT_LIBRARIES_DIRECTORY).toBe(paths.join(";"));
|
||||||
})
|
});
|
||||||
|
});
|
||||||
test("listModels", async () => {
|
|
||||||
try {
|
describe("listModels", () => {
|
||||||
await listModels();
|
const fakeModels = require("./models.json");
|
||||||
} catch(e) {}
|
const fakeModel = fakeModels[0];
|
||||||
|
const mockResponse = JSON.stringify([fakeModel]);
|
||||||
expect(fetch).toHaveBeenCalledTimes(1)
|
|
||||||
expect(fetch).toHaveBeenCalledWith(
|
let mockFetch, originalFetch;
|
||||||
"https://gpt4all.io/models/models.json"
|
|
||||||
|
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