diff --git a/README.md b/README.md
index c70e7f2f5..7b7e4384c 100644
--- a/README.md
+++ b/README.md
@@ -70,25 +70,25 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
| Experimental (Nightly Build) |
-
+
jan.exe
|
-
+
Intel
|
-
+
M1/M2
|
-
+
jan.deb
diff --git a/USAGE.md b/USAGE.md
new file mode 100644
index 000000000..be2c61262
--- /dev/null
+++ b/USAGE.md
@@ -0,0 +1,60 @@
+## Requirements for running Jan App in GPU mode on Windows and Linux
+- You must have an NVIDIA driver that supports CUDA 11.4 or higher. Refer [here](https://docs.nvidia.com/deploy/cuda-compatibility/index.html#binary-compatibility__table-toolkit-driver).
+ To check if the NVIDIA driver is installed, open PowerShell or Terminal and enter the following command:
+ ```bash
+ nvidia-smi
+ ```
+ If you see a result similar to the following, you have successfully installed the NVIDIA driver:
+ ```bash
+ +-----------------------------------------------------------------------------+
+ | NVIDIA-SMI 470.57.02 Driver Version: 470.57.02 CUDA Version: 11.4 |
+ |-------------------------------+----------------------+----------------------+
+ | GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
+ | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
+ | | | MIG M. |
+ |===============================+======================+======================|
+ | 0 NVIDIA GeForce ... Off | 00000000:01:00.0 On | N/A |
+ | 0% 51C P8 10W / 170W | 364MiB / 7982MiB | 0% Default |
+ | | | N/A |
+ +-------------------------------+----------------------+----------------------+
+ ```
+
+- You must have CUDA 11.4 or higher.
+ To check if CUDA is installed, open PowerShell or Terminal and enter the following command:
+ ```bash
+ nvcc --version
+ ```
+ If you see a result similar to the following, you have successfully installed CUDA:
+ ```bash
+ nvcc: NVIDIA (R) Cuda compiler driver
+
+ Cuda compilation tools, release 11.4, V11.4.100
+ Build cuda_11.4.r11.4/compiler.30033411_0
+ ```
+
+- Specifically for Linux, you will need to have a CUDA compatible driver, refer [here](https://docs.nvidia.com/deploy/cuda-compatibility/index.html#binary-compatibility__table-toolkit-driver), and you must add the `.so` libraries of CUDA and the CUDA compatible driver to the `LD_LIBRARY_PATH` environment variable, refer [here](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#post-installation-actions).
+
+## How to switch mode CPU/GPU Jan app
+
+By default, Jan app will run in CPU mode. When starting Jan app, the program will automatically check if your computer meets the requirements to run in GPU mode. If it does, we will automatically enable GPU mode and pick the GPU has highest VGRAM for you (feature allowing users to select one or more GPU devices for use - currently in planning). You can check whether you are using CPU mode or GPU mode in the settings/advance section of Jan app. (see image below). 
+
+If you have GPU mode but it is not enabled by default, the following possibilities may exist, you can follow the next steps to fix the error:
+
+1. You have not installed the NVIDIA driver, refer to the NVIDIA driver that supports CUDA 11.4 [here](https://docs.nvidia.com/deploy/cuda-compatibility/index.html#binary-compatibility__table-toolkit-driver).
+
+2. You have not installed the CUDA toolkit or your CUDA toolkit is not compatible with the NVIDIA driver, refer to CUDA compatibility [here](https://docs.nvidia.com/deploy/cuda-compatibility/index.html#binary-compatibility__table-toolkit-driver).
+
+3. You have not installed a CUDA compatible driver, refer [here](https://docs.nvidia.com/deploy/cuda-compatibility/index.html#binary-compatibility__table-toolkit-driver), and you must add the `.so` libraries of CUDA and the CUDA compatible driver to the `LD_LIBRARY_PATH` environment variable, refer [here](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#post-installation-actions). For Windows, add the `.dll` libraries of CUDA and the CUDA compatible driver to the `PATH` environment variable. Usually, when installing CUDA on Windows, this environment variable is automatically added, but if you do not see it, you can add it manually by referring [here](https://docs.nvidia.com/cuda/cuda-installation-guide-microsoft-windows/index.html#environment-setup).
+
+## To check the current GPU-related settings that Jan app has detected, you can go to the Settings/Advanced section as shown in the image below.
+
+
+
+
+
+When you have an issue with GPU mode, share the `settings.json` with us will help us to solve the problem faster.
+
+## Tested on
+
+- Windows 11 Pro 64-bit, NVIDIA GeForce RTX 4070ti GPU, CUDA 12.2, NVIDIA driver 531.18
+- Ubuntu 22.04 LTS, NVIDIA GeForce RTX 4070ti GPU, CUDA 12.2, NVIDIA driver 545
diff --git a/core/src/api/index.ts b/core/src/api/index.ts
index be1b06777..75fc3f007 100644
--- a/core/src/api/index.ts
+++ b/core/src/api/index.ts
@@ -53,9 +53,10 @@ export enum FileSystemRoute {
writeFileSync = 'writeFileSync',
}
export enum FileManagerRoute {
- synceFile = 'syncFile',
+ syncFile = 'syncFile',
getUserSpace = 'getUserSpace',
getResourcePath = 'getResourcePath',
+ fileStat = 'fileStat',
}
export type ApiFunction = (...args: any[]) => any
diff --git a/core/src/core.ts b/core/src/core.ts
index 2cfd43a39..11683bdc5 100644
--- a/core/src/core.ts
+++ b/core/src/core.ts
@@ -1,3 +1,5 @@
+import { FileStat } from './types'
+
/**
* Execute a extension module function in main process
*
@@ -74,6 +76,15 @@ const openExternalUrl: (url: string) => Promise = (url) =>
*/
const getResourcePath: () => Promise = () => global.core.api?.getResourcePath()
+/**
+ * Gets the file's stats.
+ *
+ * @param path - The path to the file.
+ * @returns {Promise} - A promise that resolves with the file's stats.
+ */
+const fileStat: (path: string) => Promise = (path) =>
+ global.core.api?.fileStat(path)
+
/**
* Register extension point function type definition
*/
@@ -97,4 +108,5 @@ export {
joinPath,
openExternalUrl,
baseName,
+ fileStat,
}
diff --git a/core/src/node/api/routes/fileManager.ts b/core/src/node/api/routes/fileManager.ts
new file mode 100644
index 000000000..04ab1913b
--- /dev/null
+++ b/core/src/node/api/routes/fileManager.ts
@@ -0,0 +1,12 @@
+import { FileManagerRoute } from '../../../api'
+import { HttpServer } from '../../index'
+
+export const fsRouter = async (app: HttpServer) => {
+ app.post(`/app/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {})
+
+ app.post(`/app/${FileManagerRoute.getUserSpace}`, async (request: any, reply: any) => {})
+
+ app.post(`/app/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => {})
+
+ app.post(`/app/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {})
+}
diff --git a/core/src/types/file/index.ts b/core/src/types/file/index.ts
new file mode 100644
index 000000000..6526cfc6d
--- /dev/null
+++ b/core/src/types/file/index.ts
@@ -0,0 +1,4 @@
+export type FileStat = {
+ isDirectory: boolean
+ size: number
+}
diff --git a/core/src/types/index.ts b/core/src/types/index.ts
index a4722ba78..5fb4448f9 100644
--- a/core/src/types/index.ts
+++ b/core/src/types/index.ts
@@ -4,3 +4,4 @@ export * from './thread'
export * from './message'
export * from './inference'
export * from './monitoring'
+export * from './file'
diff --git a/docs/static/img/usage/jan-gpu-enable-setting.png b/docs/static/img/usage/jan-gpu-enable-setting.png
new file mode 100644
index 000000000..9a65539c0
Binary files /dev/null and b/docs/static/img/usage/jan-gpu-enable-setting.png differ
diff --git a/docs/static/img/usage/jan-open-home-directory.png b/docs/static/img/usage/jan-open-home-directory.png
new file mode 100644
index 000000000..17cc97534
Binary files /dev/null and b/docs/static/img/usage/jan-open-home-directory.png differ
diff --git a/docs/static/img/usage/jan-open-settings-1.png b/docs/static/img/usage/jan-open-settings-1.png
new file mode 100644
index 000000000..f26ba925a
Binary files /dev/null and b/docs/static/img/usage/jan-open-settings-1.png differ
diff --git a/docs/static/img/usage/jan-open-settings-2.png b/docs/static/img/usage/jan-open-settings-2.png
new file mode 100644
index 000000000..f95689229
Binary files /dev/null and b/docs/static/img/usage/jan-open-settings-2.png differ
diff --git a/docs/static/img/usage/jan-open-settings-3.png b/docs/static/img/usage/jan-open-settings-3.png
new file mode 100644
index 000000000..97ecb1684
Binary files /dev/null and b/docs/static/img/usage/jan-open-settings-3.png differ
diff --git a/electron/handlers/fileManager.ts b/electron/handlers/fileManager.ts
index 2a78deaf9..f8b8ee6f1 100644
--- a/electron/handlers/fileManager.ts
+++ b/electron/handlers/fileManager.ts
@@ -4,14 +4,17 @@ import reflect from '@alumna/reflect'
import { FileManagerRoute } from '@janhq/core'
import { userSpacePath, getResourcePath } from './../utils/path'
+import fs from 'fs'
+import { join } from 'path'
+import { FileStat } from '@janhq/core/.'
/**
* Handles file system extensions operations.
*/
export function handleFileMangerIPCs() {
- // Handles the 'synceFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path.
+ // Handles the 'syncFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path.
ipcMain.handle(
- FileManagerRoute.synceFile,
+ FileManagerRoute.syncFile,
async (_event, src: string, dest: string) => {
return reflect({
src,
@@ -31,7 +34,33 @@ export function handleFileMangerIPCs() {
)
// Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path.
- ipcMain.handle(FileManagerRoute.getResourcePath, async (_event) => {
- return getResourcePath()
- })
+ ipcMain.handle(FileManagerRoute.getResourcePath, async (_event) =>
+ getResourcePath()
+ )
+
+ // handle fs is directory here
+ ipcMain.handle(
+ FileManagerRoute.fileStat,
+ async (_event, path: string): Promise => {
+ const normalizedPath = path
+ .replace(`file://`, '')
+ .replace(`file:/`, '')
+ .replace(`file:\\\\`, '')
+ .replace(`file:\\`, '')
+
+ const fullPath = join(userSpacePath, normalizedPath)
+ const isExist = fs.existsSync(fullPath)
+ if (!isExist) return undefined
+
+ const isDirectory = fs.lstatSync(fullPath).isDirectory()
+ const size = fs.statSync(fullPath).size
+
+ const fileStat: FileStat = {
+ isDirectory,
+ size,
+ }
+
+ return fileStat
+ }
+ )
}
diff --git a/extensions/inference-nitro-extension/bin/version.txt b/extensions/inference-nitro-extension/bin/version.txt
index 28d007539..9dd179330 100644
--- a/extensions/inference-nitro-extension/bin/version.txt
+++ b/extensions/inference-nitro-extension/bin/version.txt
@@ -1 +1 @@
-0.1.32
+0.1.34
diff --git a/extensions/inference-nitro-extension/download.bat b/extensions/inference-nitro-extension/download.bat
index 1776b7dfe..f82b49c2e 100644
--- a/extensions/inference-nitro-extension/download.bat
+++ b/extensions/inference-nitro-extension/download.bat
@@ -1,3 +1,3 @@
@echo off
set /p NITRO_VERSION=<./bin/version.txt
-.\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda.tar.gz -e --strip 1 -o ./bin/win-cuda && .\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.tar.gz -e --strip 1 -o ./bin/win-cpu
+.\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda-12-0.tar.gz -e --strip 1 -o ./bin/win-cuda-12-0 && .\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda-11-4.tar.gz -e --strip 1 -o ./bin/win-cuda-11-4 && .\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.tar.gz -e --strip 1 -o ./bin/win-cpu
diff --git a/extensions/inference-nitro-extension/package.json b/extensions/inference-nitro-extension/package.json
index a35c51e58..479cc51dc 100644
--- a/extensions/inference-nitro-extension/package.json
+++ b/extensions/inference-nitro-extension/package.json
@@ -8,7 +8,7 @@
"license": "AGPL-3.0",
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
- "downloadnitro:linux": "NITRO_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.tar.gz -e --strip 1 -o ./bin/linux-cpu && chmod +x ./bin/linux-cpu/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.tar.gz -e --strip 1 -o ./bin/linux-cuda && chmod +x ./bin/linux-cuda/nitro",
+ "downloadnitro:linux": "NITRO_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.tar.gz -e --strip 1 -o ./bin/linux-cpu && chmod +x ./bin/linux-cpu/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda-12-0.tar.gz -e --strip 1 -o ./bin/linux-cuda-12-0 && chmod +x ./bin/linux-cuda-12-0/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda-11-4.tar.gz -e --strip 1 -o ./bin/linux-cuda-11-4 && chmod +x ./bin/linux-cuda-11-4/nitro",
"downloadnitro:darwin": "NITRO_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.tar.gz -e --strip 1 -o ./bin/mac-arm64 && chmod +x ./bin/mac-arm64/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.tar.gz -e --strip 1 -o ./bin/mac-x64 && chmod +x ./bin/mac-x64/nitro",
"downloadnitro:win32": "download.bat",
"downloadnitro": "run-script-os",
diff --git a/extensions/inference-nitro-extension/src/helpers/sse.ts b/extensions/inference-nitro-extension/src/helpers/sse.ts
index a7b35f2f0..c6352383d 100644
--- a/extensions/inference-nitro-extension/src/helpers/sse.ts
+++ b/extensions/inference-nitro-extension/src/helpers/sse.ts
@@ -30,7 +30,10 @@ export function requestInference(
signal: controller?.signal,
})
.then(async (response) => {
- if (model.parameters.stream) {
+ if (model.parameters.stream === false) {
+ const data = await response.json();
+ subscriber.next(data.choices[0]?.message?.content ?? "");
+ } else {
const stream = response.body;
const decoder = new TextDecoder("utf-8");
const reader = stream?.getReader();
@@ -54,9 +57,6 @@ export function requestInference(
}
}
}
- } else {
- const data = await response.json();
- subscriber.next(data.choices[0]?.message?.content ?? "");
}
subscriber.complete();
})
diff --git a/extensions/inference-nitro-extension/src/module.ts b/extensions/inference-nitro-extension/src/module.ts
index 7678bae19..44c6b9100 100644
--- a/extensions/inference-nitro-extension/src/module.ts
+++ b/extensions/inference-nitro-extension/src/module.ts
@@ -85,28 +85,40 @@ function checkFileExistenceInPaths(file: string, paths: string[]): boolean {
}
function updateCudaExistence() {
- let files: string[];
+ let filesCuda12: string[];
+ let filesCuda11: string[];
let paths: string[];
+ let cudaVersion: string = "";
if (process.platform === "win32") {
- files = ["cublas64_12.dll", "cudart64_12.dll", "cublasLt64_12.dll"];
+ filesCuda12 = ["cublas64_12.dll", "cudart64_12.dll", "cublasLt64_12.dll"];
+ filesCuda11 = ["cublas64_11.dll", "cudart64_11.dll", "cublasLt64_11.dll"];
paths = process.env.PATH ? process.env.PATH.split(path.delimiter) : [];
- const nitro_cuda_path = path.join(__dirname, "bin", "win-cuda");
- paths.push(nitro_cuda_path);
} else {
- files = ["libcudart.so.12", "libcublas.so.12", "libcublasLt.so.12"];
+ filesCuda12 = ["libcudart.so.12", "libcublas.so.12", "libcublasLt.so.12"];
+ filesCuda11 = ["libcudart.so.11.0", "libcublas.so.11", "libcublasLt.so.11"];
paths = process.env.LD_LIBRARY_PATH
? process.env.LD_LIBRARY_PATH.split(path.delimiter)
: [];
- const nitro_cuda_path = path.join(__dirname, "bin", "linux-cuda");
- paths.push(nitro_cuda_path);
paths.push("/usr/lib/x86_64-linux-gnu/");
}
- let cudaExists = files.every(
+ let cudaExists = filesCuda12.every(
(file) => existsSync(file) || checkFileExistenceInPaths(file, paths)
);
+ if (!cudaExists) {
+ cudaExists = filesCuda11.every(
+ (file) => existsSync(file) || checkFileExistenceInPaths(file, paths)
+ );
+ if (cudaExists) {
+ cudaVersion = "11";
+ }
+ }
+ else {
+ cudaVersion = "12";
+ }
+
let data;
try {
data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
@@ -115,6 +127,7 @@ function updateCudaExistence() {
}
data["cuda"].exist = cudaExists;
+ data["cuda"].version = cudaVersion;
if (cudaExists) {
data.run_mode = "gpu";
}
@@ -376,12 +389,17 @@ function spawnNitroProcess(nitroResourceProbe: any): Promise {
let cudaVisibleDevices = "";
let binaryName;
if (process.platform === "win32") {
- let nvida_info = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
- if (nvida_info["run_mode"] === "cpu") {
+ let nvidiaInfo = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
+ if (nvidiaInfo["run_mode"] === "cpu") {
binaryFolder = path.join(binaryFolder, "win-cpu");
} else {
- binaryFolder = path.join(binaryFolder, "win-cuda");
- cudaVisibleDevices = nvida_info["gpu_highest_vram"];
+ if (nvidiaInfo["cuda"].version === "12") {
+ binaryFolder = path.join(binaryFolder, "win-cuda-12-0");
+ }
+ else {
+ binaryFolder = path.join(binaryFolder, "win-cuda-11-4");
+ }
+ cudaVisibleDevices = nvidiaInfo["gpu_highest_vram"];
}
binaryName = "nitro.exe";
} else if (process.platform === "darwin") {
@@ -392,12 +410,17 @@ function spawnNitroProcess(nitroResourceProbe: any): Promise {
}
binaryName = "nitro";
} else {
- let nvida_info = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
- if (nvida_info["run_mode"] === "cpu") {
+ let nvidiaInfo = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
+ if (nvidiaInfo["run_mode"] === "cpu") {
binaryFolder = path.join(binaryFolder, "linux-cpu");
} else {
- binaryFolder = path.join(binaryFolder, "linux-cuda");
- cudaVisibleDevices = nvida_info["gpu_highest_vram"];
+ if (nvidiaInfo["cuda"].version === "12") {
+ binaryFolder = path.join(binaryFolder, "linux-cuda-12-0");
+ }
+ else {
+ binaryFolder = path.join(binaryFolder, "linux-cuda-11-4");
+ }
+ cudaVisibleDevices = nvidiaInfo["gpu_highest_vram"];
}
binaryName = "nitro";
}
diff --git a/extensions/inference-openai-extension/src/helpers/sse.ts b/extensions/inference-openai-extension/src/helpers/sse.ts
index bbfc5e1fd..fb75816e7 100644
--- a/extensions/inference-openai-extension/src/helpers/sse.ts
+++ b/extensions/inference-openai-extension/src/helpers/sse.ts
@@ -46,7 +46,10 @@ export function requestInference(
subscriber.complete();
return;
}
- if (model.parameters.stream) {
+ if (model.parameters.stream === false) {
+ const data = await response.json();
+ subscriber.next(data.choices[0]?.message?.content ?? "");
+ } else {
const stream = response.body;
const decoder = new TextDecoder("utf-8");
const reader = stream?.getReader();
@@ -70,9 +73,6 @@ export function requestInference(
}
}
}
- } else {
- const data = await response.json();
- subscriber.next(data.choices[0]?.message?.content ?? "");
}
subscriber.complete();
})
diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts
index 8c8972dac..b0fc995a1 100644
--- a/extensions/model-extension/src/index.ts
+++ b/extensions/model-extension/src/index.ts
@@ -5,11 +5,12 @@ import {
abortDownload,
getResourcePath,
getUserSpace,
+ fileStat,
InferenceEngine,
joinPath,
+ ModelExtension,
+ Model,
} from '@janhq/core'
-import { ModelExtension, Model } from '@janhq/core'
-import { baseName } from '@janhq/core/.'
/**
* A extension for models
@@ -21,6 +22,9 @@ export default class JanModelExtension implements ModelExtension {
private static readonly _incompletedModelFileName = '.download'
private static readonly _offlineInferenceEngine = InferenceEngine.nitro
+ private static readonly _configDirName = 'config'
+ private static readonly _defaultModelFileName = 'default-model.json'
+
/**
* Implements type from JanExtension.
* @override
@@ -199,7 +203,7 @@ export default class JanModelExtension implements ModelExtension {
): Promise {
try {
if (!(await fs.existsSync(JanModelExtension._homeDir))) {
- console.debug('model folder not found')
+ console.error('Model folder not found')
return []
}
@@ -220,13 +224,22 @@ export default class JanModelExtension implements ModelExtension {
dirName,
JanModelExtension._modelMetadataFileName,
])
- let model = await this.readModelMetadata(jsonPath)
- model = typeof model === 'object' ? model : JSON.parse(model)
- if (selector && !(await selector?.(dirName, model))) {
- return
+ if (await fs.existsSync(jsonPath)) {
+ // if we have the model.json file, read it
+ let model = await this.readModelMetadata(jsonPath)
+ model = typeof model === 'object' ? model : JSON.parse(model)
+
+ if (selector && !(await selector?.(dirName, model))) {
+ return
+ }
+ return model
+ } else {
+ // otherwise, we generate our own model file
+ // TODO: we might have more than one binary file here. This will be addressed with new version of Model file
+ // which is the PR from Hiro on branch Jan can see
+ return this.generateModelMetadata(dirName)
}
- return model
})
const results = await Promise.allSettled(readJsonPromises)
const modelData = results.map((result) => {
@@ -254,6 +267,84 @@ export default class JanModelExtension implements ModelExtension {
return fs.readFileSync(path, 'utf-8')
}
+ /**
+ * Handle the case where we have the model directory but we don't have the corresponding
+ * model.json file associated with it.
+ *
+ * This function will create a model.json file for the model.
+ *
+ * @param dirName the director which reside in ~/jan/models but does not have model.json file.
+ */
+ private async generateModelMetadata(dirName: string): Promise {
+ const files: string[] = await fs.readdirSync(
+ await joinPath([JanModelExtension._homeDir, dirName])
+ )
+
+ // sort files by name
+ files.sort()
+
+ // find the first file which is not a directory
+ let binaryFileName: string | undefined = undefined
+ let binaryFileSize: number | undefined = undefined
+
+ for (const file of files) {
+ if (file.endsWith(JanModelExtension._incompletedModelFileName)) continue
+ if (file.endsWith('.json')) continue
+
+ const path = await joinPath([JanModelExtension._homeDir, dirName, file])
+ const fileStats = await fileStat(path)
+ if (fileStats.isDirectory) continue
+ binaryFileSize = fileStats.size
+ binaryFileName = file
+ break
+ }
+
+ if (!binaryFileName) {
+ console.warn(`Unable to find binary file for model ${dirName}`)
+ return
+ }
+
+ const defaultModel = await this.getDefaultModel()
+ if (!defaultModel) {
+ console.error('Unable to find default model')
+ return
+ }
+
+ const model: Model = {
+ ...defaultModel,
+ id: dirName,
+ name: dirName,
+ created: Date.now(),
+ description: `${dirName} - user self import model`,
+ }
+
+ const modelFilePath = await joinPath([
+ JanModelExtension._homeDir,
+ dirName,
+ JanModelExtension._modelMetadataFileName,
+ ])
+
+ await fs.writeFileSync(modelFilePath, JSON.stringify(model, null, 2))
+
+ return model
+ }
+
+ private async getDefaultModel(): Promise {
+ const defaultModelPath = await joinPath([
+ JanModelExtension._homeDir,
+ JanModelExtension._configDirName,
+ JanModelExtension._defaultModelFileName,
+ ])
+
+ if (!(await fs.existsSync(defaultModelPath))) {
+ return undefined
+ }
+
+ const model = await this.readModelMetadata(defaultModelPath)
+
+ return typeof model === 'object' ? model : JSON.parse(model)
+ }
+
/**
* Gets all available models.
* @returns A Promise that resolves with an array of all models.
diff --git a/models/config/default-model.json b/models/config/default-model.json
new file mode 100644
index 000000000..a00a8c024
--- /dev/null
+++ b/models/config/default-model.json
@@ -0,0 +1,35 @@
+{
+ "object": "model",
+ "version": 1,
+ "format": "gguf",
+ "source_url": "N/A",
+ "id": "N/A",
+ "name": "N/A",
+ "created": 0,
+ "description": "User self import model",
+ "settings": {
+ "ctx_len": 4096,
+ "ngl": 0,
+ "embedding": false,
+ "n_parallel": 0,
+ "cpu_threads": 0,
+ "prompt_template": ""
+ },
+ "parameters": {
+ "temperature": 0,
+ "token_limit": 0,
+ "top_k": 0,
+ "top_p": 0,
+ "stream": false,
+ "max_tokens": 4096,
+ "stop": [],
+ "frequency_penalty": 0,
+ "presence_penalty": 0
+ },
+ "metadata": {
+ "author": "User",
+ "tags": [],
+ "size": 0
+ },
+ "engine": "nitro"
+}
diff --git a/server/package.json b/server/package.json
index 36fdc124f..4db9894be 100644
--- a/server/package.json
+++ b/server/package.json
@@ -17,6 +17,7 @@
"build": "tsc"
},
"dependencies": {
+ "@alumna/reflect": "^1.1.3",
"@fastify/cors": "^8.4.2",
"@fastify/static": "^6.12.0",
"@fastify/swagger": "^8.13.0",
diff --git a/web/containers/GPUDriverPromptModal/index.tsx b/web/containers/GPUDriverPromptModal/index.tsx
index 68efa33d5..315d2395b 100644
--- a/web/containers/GPUDriverPromptModal/index.tsx
+++ b/web/containers/GPUDriverPromptModal/index.tsx
@@ -36,28 +36,23 @@ const GPUDriverPrompt: React.FC = () => {
- Missing Nvidia Driver and Cuda Toolkit
+
+ Checking for machine that does not meet the requirements.
+
- It seems like you are missing Nvidia Driver or Cuda Toolkit or both.
- Please follow the instructions on the{' '}
+ It appears that you are missing some dependencies required to run in
+ GPU mode. Please follow the instructions below for more details{' '}
- openExternalUrl('https://developer.nvidia.com/cuda-toolkit')
+ openExternalUrl(
+ 'https://github.com/janhq/jan/blob/main/USAGE.md'
+ )
}
>
- NVidia Cuda Toolkit Installation Page
+ Jan running mode documentation
{' '}
- and the{' '}
-
- openExternalUrl('https://www.nvidia.com/Download/index.aspx')
- }
- >
- Nvidia Driver Installation Page
-
.
diff --git a/web/containers/Providers/EventListener.tsx b/web/containers/Providers/EventListener.tsx
index b82efb066..d91a877d6 100644
--- a/web/containers/Providers/EventListener.tsx
+++ b/web/containers/Providers/EventListener.tsx
@@ -22,8 +22,12 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
const modelsRef = useRef(models)
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
- const { setDownloadState, setDownloadStateSuccess, setDownloadStateFailed } =
- useDownloadState()
+ const {
+ setDownloadState,
+ setDownloadStateSuccess,
+ setDownloadStateFailed,
+ setDownloadStateCancelled,
+ } = useDownloadState()
const downloadedModelRef = useRef(downloadedModels)
useEffect(() => {
@@ -52,13 +56,18 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
window.electronAPI.onFileDownloadError(
async (_event: string, state: any) => {
- if (state.err?.message !== 'aborted')
- console.error('Download error', state)
const modelName = await baseName(state.fileName)
const model = modelsRef.current.find(
(model) => modelBinFileName(model) === modelName
)
- if (model) setDownloadStateFailed(model.id)
+ if (model) {
+ if (state.err?.message !== 'aborted') {
+ console.error('Download error', state)
+ setDownloadStateFailed(model.id, state.err.message)
+ } else {
+ setDownloadStateCancelled(model.id)
+ }
+ }
}
)
diff --git a/web/hooks/useDownloadState.ts b/web/hooks/useDownloadState.ts
index 0d7967ac5..bf5177228 100644
--- a/web/hooks/useDownloadState.ts
+++ b/web/hooks/useDownloadState.ts
@@ -29,26 +29,49 @@ const setDownloadStateSuccessAtom = atom(null, (get, set, modelId: string) => {
})
})
-const setDownloadStateFailedAtom = atom(null, (get, set, modelId: string) => {
- const currentState = { ...get(modelDownloadStateAtom) }
- const state = currentState[modelId]
- if (!state) {
- console.debug(`Cannot find download state for ${modelId}`)
- toaster({
- title: 'Cancel Download',
- description: `Model ${modelId} cancel download`,
- })
- return
+const setDownloadStateFailedAtom = atom(
+ null,
+ (get, set, modelId: string, error: string) => {
+ const currentState = { ...get(modelDownloadStateAtom) }
+ const state = currentState[modelId]
+ if (!state) {
+ console.debug(`Cannot find download state for ${modelId}`)
+ toaster({
+ title: 'Download Failed',
+ description: `Model ${modelId} download failed: ${error}`,
+ type: 'error',
+ })
+ return
+ }
+ delete currentState[modelId]
+ set(modelDownloadStateAtom, currentState)
}
- delete currentState[modelId]
- set(modelDownloadStateAtom, currentState)
-})
+)
+const setDownloadStateCancelledAtom = atom(
+ null,
+ (get, set, modelId: string) => {
+ const currentState = { ...get(modelDownloadStateAtom) }
+ const state = currentState[modelId]
+ if (!state) {
+ console.debug(`Cannot find download state for ${modelId}`)
+ toaster({
+ title: 'Cancel Download',
+ description: `Model ${modelId} cancel download`,
+ })
+
+ return
+ }
+ delete currentState[modelId]
+ set(modelDownloadStateAtom, currentState)
+ }
+)
export function useDownloadState() {
const modelDownloadState = useAtomValue(modelDownloadStateAtom)
const setDownloadState = useSetAtom(setDownloadStateAtom)
const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom)
const setDownloadStateFailed = useSetAtom(setDownloadStateFailedAtom)
+ const setDownloadStateCancelled = useSetAtom(setDownloadStateCancelledAtom)
const downloadStates: DownloadState[] = []
for (const [, value] of Object.entries(modelDownloadState)) {
@@ -61,6 +84,7 @@ export function useDownloadState() {
setDownloadState,
setDownloadStateSuccess,
setDownloadStateFailed,
+ setDownloadStateCancelled,
downloadStates,
}
}
diff --git a/web/screens/Chat/SimpleTextMessage/index.tsx b/web/screens/Chat/SimpleTextMessage/index.tsx
index 75ce5b24a..11e34442a 100644
--- a/web/screens/Chat/SimpleTextMessage/index.tsx
+++ b/web/screens/Chat/SimpleTextMessage/index.tsx
@@ -5,7 +5,7 @@ import { ChatCompletionRole, MessageStatus, ThreadMessage } from '@janhq/core'
import hljs from 'highlight.js'
import { useAtomValue } from 'jotai'
-import { Marked } from 'marked'
+import { Marked, Renderer } from 'marked'
import { markedHighlight } from 'marked-highlight'
@@ -30,7 +30,7 @@ const SimpleTextMessage: React.FC = (props) => {
}
const clipboard = useClipboard({ timeout: 1000 })
- const marked = new Marked(
+ const marked: Marked = new Marked(
markedHighlight({
langPrefix: 'hljs',
highlight(code, lang) {
@@ -46,6 +46,11 @@ const SimpleTextMessage: React.FC = (props) => {
}),
{
renderer: {
+ link: (href, title, text) => {
+ return Renderer.prototype.link
+ ?.apply(this, [href, title, text])
+ .replace('
diff --git a/web/screens/ExploreModels/ExploreModelList/index.tsx b/web/screens/ExploreModels/ExploreModelList/index.tsx
index 471896cb3..54c8120dd 100644
--- a/web/screens/ExploreModels/ExploreModelList/index.tsx
+++ b/web/screens/ExploreModels/ExploreModelList/index.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/naming-convention */
import { Model } from '@janhq/core'
import ExploreModelItem from '@/screens/ExploreModels/ExploreModelItem'
@@ -8,33 +7,45 @@ type Props = {
}
const ExploreModelList: React.FC = ({ models }) => {
- const sortOrder: Record = {
- '7b': 1,
- '13b': 2,
- '34b': 3,
- '70b': 4,
- '120b': 5,
- 'tiny': 6,
- }
- const sortedModels = models?.sort((a, b) => {
- const aIsFeatured = a.metadata.tags.includes('Featured')
- const bIsFeatured = b.metadata.tags.includes('Featured')
- const aIsRecommended = a.metadata.tags.includes('Recommended')
- const bIsRecommended = b.metadata.tags.includes('Recommended')
- const aNumericTag =
- a.metadata.tags.find((tag) => !!sortOrder[tag.toLowerCase()]) ?? 'Tiny'
- const bNumericTag =
- b.metadata.tags.find((tag) => !!sortOrder[tag.toLowerCase()]) ?? 'Tiny'
+ const takenModelIds: string[] = []
+ const featuredModels = models
+ .filter((m) => {
+ if (m.metadata.tags.includes('Featured')) {
+ takenModelIds.push(m.id)
+ return m
+ }
+ })
+ .sort((m1, m2) => m1.metadata.size - m2.metadata.size)
+
+ const recommendedModels = models
+ .filter((m) => {
+ if (m.metadata.tags.includes('Recommended')) {
+ takenModelIds.push(m.id)
+ return m
+ }
+ })
+ .sort((m1, m2) => m1.metadata.size - m2.metadata.size)
+
+ const openAiModels = models
+ .filter((m) => {
+ if (m.engine === 'openai') {
+ takenModelIds.push(m.id)
+ return m
+ }
+ })
+ .sort((m1: Model, m2: Model) => m1.name.localeCompare(m2.name))
+
+ const remainingModels = models
+ .filter((m) => !takenModelIds.includes(m.id))
+ .sort((m1, m2) => m1.metadata.size - m2.metadata.size)
+
+ const sortedModels: Model[] = [
+ ...featuredModels,
+ ...recommendedModels,
+ ...openAiModels,
+ ...remainingModels,
+ ]
- if (aIsFeatured !== bIsFeatured) return aIsFeatured ? -1 : 1
- if (aNumericTag !== bNumericTag)
- return (
- sortOrder[aNumericTag.toLowerCase()] -
- sortOrder[bNumericTag.toLowerCase()]
- )
- if (aIsRecommended !== bIsRecommended) return aIsRecommended ? -1 : 1
- return a.metadata.size - b.metadata.size
- })
return (
{sortedModels?.map((model) => (
diff --git a/web/screens/ExploreModels/index.tsx b/web/screens/ExploreModels/index.tsx
index cc053239f..d988fcafc 100644
--- a/web/screens/ExploreModels/index.tsx
+++ b/web/screens/ExploreModels/index.tsx
@@ -1,5 +1,6 @@
import { useState } from 'react'
+import { openExternalUrl } from '@janhq/core'
import {
Input,
ScrollArea,
@@ -44,6 +45,10 @@ const ExploreModelsScreen = () => {
}
})
+ const onHowToImportModelClick = () => {
+ openExternalUrl('https://jan.ai/guides/using-models/import-manually/')
+ }
+
if (loading) return
return (
@@ -72,13 +77,12 @@ const ExploreModelsScreen = () => {
/>
|