* feat: integrating cortex * Temporary prevent crash Signed-off-by: James <namnh0122@gmail.com> * fix yarn lint Signed-off-by: James <namnh0122@gmail.com> * refactor: remove core node module - fs - extensions and so on (#3151) * add migration script for threads, messages and models Signed-off-by: James <namnh0122@gmail.com> * remove freq_penalty and presence_penalty if model not supported Signed-off-by: James <namnh0122@gmail.com> * add back models in my models Signed-off-by: James <namnh0122@gmail.com> * fix api-url for setup API key popup Signed-off-by: James <namnh0122@gmail.com> * fix using model name for dropdown model Signed-off-by: James <namnh0122@gmail.com> * fix can't click to hotkey Signed-off-by: James <namnh0122@gmail.com> * fix: disable some UIs Signed-off-by: James <namnh0122@gmail.com> * fix build Signed-off-by: James <namnh0122@gmail.com> * reduce calling HF api Signed-off-by: James <namnh0122@gmail.com> * some ui update Signed-off-by: James <namnh0122@gmail.com> * feat: modal migration UI (#3153) * feat: handle popup migration * chore: update loader * chore: integrate script migration * chore: cleanup import * chore: moving out spinner loader * chore: update check thread message success migrate * chore: add handle script into retry button * remove warning from joi Signed-off-by: James <namnh0122@gmail.com> * chore: fix duplicate children * fix: path after migrating model Signed-off-by: James <namnh0122@gmail.com> * chore: apply mutation for config * chore: prevent calling too many create assistant api Signed-off-by: James <namnh0122@gmail.com> * using cortexso Signed-off-by: James <namnh0122@gmail.com> * update download api Signed-off-by: James <namnh0122@gmail.com> * fix use on slider item Signed-off-by: James <namnh0122@gmail.com> * fix: ui no download model or simple onboarding (#3166) * fix download huggingface model match with slider item Signed-off-by: James <namnh0122@gmail.com> * update owner_logo to logo and author Signed-off-by: James <namnh0122@gmail.com> * update new cortexso Signed-off-by: James <namnh0122@gmail.com> * Add install python step for macos * add engine table Signed-off-by: James <namnh0122@gmail.com> * fix local icons Signed-off-by: James <namnh0122@gmail.com> * feat: add search feature for model hub Signed-off-by: James <namnh0122@gmail.com> * fix misalign switch Signed-off-by: James <namnh0122@gmail.com> * fix: delete thread not focus on other thread Signed-off-by: James <namnh0122@gmail.com> * add get model from hugging face Signed-off-by: James <namnh0122@gmail.com> * fix download from hugging face Signed-off-by: James <namnh0122@gmail.com> * small update Signed-off-by: James <namnh0122@gmail.com> * update Signed-off-by: James <namnh0122@gmail.com> * fix system monitor rounded only on the left Signed-off-by: James <namnh0122@gmail.com> * chore: update ui new hub screen (#3174) * chore: update ui new hub screen * chore: update layout centerpanel thread and hub screen * chore: update detail model by group * update cortexso 0.1.13 Signed-off-by: James <namnh0122@gmail.com> * chore: add file size Signed-off-by: James <namnh0122@gmail.com> * chore: put engine to experimental feature Signed-off-by: James <namnh0122@gmail.com> * chore: open cortex folder Signed-off-by: James <namnh0122@gmail.com> * chore: add back user avatar Signed-off-by: James <namnh0122@gmail.com> * chore: minor UI hub (#3182) * chore: add back right click thread list and update 3 dots are overlapping with the text * chore: update position dropdown list my models * chore: make on-device tab showing 6 items instead of 4 * chore: update style description modals detail model * chore: update isGeneration loader and author name on modal * feat: integrate cortex single executable Signed-off-by: James <namnh0122@gmail.com> * fix build Signed-off-by: James <namnh0122@gmail.com> * chore: added blank state * chore: update ui component blank state * bump cortex binary version * fix: logic show modal migration (#3165) * fix: logic show modal migration * chore: fixed logic * chore: read contain format gguf local models * chore: change return hasLocalModel * chore: intiial skipmigration state * chore: filter embedding model * fix: delete top thread not focus on any other thread * chore: added UI no result component search models group (#3188) * fix: remote model should show all when user config that engine Signed-off-by: James <namnh0122@gmail.com> * chore: set state thread and models migration using getOnInit (#3189) * chore: set state thread and models migration using getOnInit * chore: add state as dependecies hooks * chore: system monitor panel show engine model (#3192) * fix: remove config api, replace with engine Signed-off-by: James <namnh0122@gmail.com> * update Signed-off-by: James <namnh0122@gmail.com> * update reactquery Signed-off-by: James <namnh0122@gmail.com> * bump cortex 0.4.35 * feat: add waiting for cortex popup Signed-off-by: James <namnh0122@gmail.com> * chore: add loader detail model popup (#3195) * chore: model start loader (#3197) * chore: added model loader when user starting chat without model active * chore: update copies loader * fix: select min file size if recommended quant does not exist Signed-off-by: James <namnh0122@gmail.com> * chore: temporary hide gpu config * fix: tensorrt not shown Signed-off-by: James <namnh0122@gmail.com> * fix lint Signed-off-by: James <namnh0122@gmail.com> * fix tests Signed-off-by: James <namnh0122@gmail.com> * fix e2e tests (wip) Signed-off-by: James <namnh0122@gmail.com> * update Signed-off-by: James <namnh0122@gmail.com> * fix: adding element and correct test to adapt new UI * fix: temp skip unstable part * fix: only show models which can be supported Signed-off-by: James <namnh0122@gmail.com> * Update version.txt * update send message Signed-off-by: James <namnh0122@gmail.com> * fix: not allow user send message when is generating Signed-off-by: James <namnh0122@gmail.com> * chore: temp skip Playwright test due to env issue * chore: temp skip Playwright test due to env issue * update Signed-off-by: James <namnh0122@gmail.com> * chore: minor-ui-feedback (#3202) --------- Signed-off-by: James <namnh0122@gmail.com> Co-authored-by: Louis <louis@jan.ai> Co-authored-by: Faisal Amir <urmauur@gmail.com> Co-authored-by: Hien To <tominhhien97@gmail.com> Co-authored-by: Van Pham <64197333+Van-QA@users.noreply.github.com> Co-authored-by: Van-QA <van@jan.ai>
336 lines
8.3 KiB
TypeScript
336 lines
8.3 KiB
TypeScript
import {
|
|
downloadFile,
|
|
listFiles,
|
|
listModels,
|
|
ModelEntry,
|
|
} from '@huggingface/hub'
|
|
import {
|
|
AllQuantizations,
|
|
HuggingFaceRepoData,
|
|
LlmEngine,
|
|
Model,
|
|
RemoteEngines,
|
|
} from '@janhq/core'
|
|
import { parse } from 'yaml'
|
|
|
|
import { markdownParser } from './markdown-parser'
|
|
|
|
/**
|
|
* Try to find and read model.yml from main branch. This function is only used for HuggingFace.
|
|
*/
|
|
export const tryGettingModelYaml = async (
|
|
repoName: string,
|
|
branch?: string
|
|
): Promise<Model | undefined> => {
|
|
const revision = branch ?? 'main'
|
|
try {
|
|
for await (const fileInfo of listFiles({
|
|
repo: { type: 'model', name: repoName },
|
|
revision,
|
|
})) {
|
|
if (fileInfo.path !== 'model.yml') continue
|
|
|
|
const data = await (
|
|
await downloadFile({
|
|
repo: repoName,
|
|
revision,
|
|
path: fileInfo.path,
|
|
})
|
|
)?.text()
|
|
|
|
if (!data) return undefined
|
|
return parse(data) as Model
|
|
}
|
|
} catch (e) {
|
|
console.debug('Cannot get model info for', repoName, e)
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
export const fetchHuggingFaceModel = async (
|
|
owner: string,
|
|
modelName: string
|
|
): Promise<HfModelEntry[]> => {
|
|
const ownerAndModelName = `${owner}/${modelName}`
|
|
const result: HfModelEntry[] = []
|
|
|
|
for await (const model of listModels({
|
|
search: {
|
|
query: ownerAndModelName,
|
|
owner: owner,
|
|
},
|
|
})) {
|
|
result.push({
|
|
...model,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
export const fetchCortexHubModelEntries = async (): Promise<HfModelEntry[]> => {
|
|
const modelEntries: HfModelEntry[] = []
|
|
|
|
for await (const model of listModels({
|
|
search: { query: 'cortexso' },
|
|
})) {
|
|
modelEntries.push({
|
|
...model,
|
|
})
|
|
}
|
|
|
|
return modelEntries
|
|
}
|
|
|
|
export const fetchCortexHubModels = async (): Promise<HfModelEntry[]> => {
|
|
const modelEntries = await fetchCortexHubModelEntries()
|
|
const promises: Promise<Model | undefined>[] = []
|
|
modelEntries.forEach((model) => {
|
|
promises.push(tryGettingModelYaml(model.name))
|
|
})
|
|
|
|
const modelData = await Promise.allSettled(promises)
|
|
for (let i = 0; i < modelEntries.length; i++) {
|
|
if (modelData[i].status === 'fulfilled') {
|
|
const fulfillResult = modelData[i] as PromiseFulfilledResult<
|
|
Model | undefined
|
|
>
|
|
|
|
const model: Model = fulfillResult.value as Model
|
|
if (model) {
|
|
if (model.model_type === 'embedding') continue
|
|
modelEntries[i].model = model
|
|
const isRemoteModel =
|
|
RemoteEngines.find((engine) => model.engine === engine) != null
|
|
modelEntries[i].remoteModel = isRemoteModel
|
|
modelEntries[i].engine = model.engine
|
|
}
|
|
}
|
|
}
|
|
|
|
return modelEntries
|
|
}
|
|
|
|
export const getBranches = async (name: string): Promise<string[]> => {
|
|
try {
|
|
const response = await fetch(
|
|
`https://huggingface.co/api/models/${name}/refs`
|
|
)
|
|
const data = await response.json()
|
|
return data.branches.map((branch: { name: string }) => branch.name)
|
|
} catch (err) {
|
|
console.error('Failed to get HF branches:', err)
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Getting the total file size of a repo by repoId and branch. This will get all file size.
|
|
* So this function can't be used on a multiple model branch.
|
|
*
|
|
* @param repoId The repoId of the model
|
|
* @param branch The branch of the model
|
|
* @returns The total file size of the model
|
|
*/
|
|
const getFileSizeByRepoAndBranch = async (
|
|
repoId: string,
|
|
branch: string
|
|
): Promise<number> => {
|
|
let number = 0
|
|
for await (const fileInfo of listFiles({
|
|
repo: { type: 'model', name: repoId },
|
|
revision: branch,
|
|
})) {
|
|
number += fileInfo.size
|
|
}
|
|
|
|
return number
|
|
}
|
|
|
|
export const getEngineAndBranches = async (
|
|
name: string
|
|
): Promise<EngineToBranches> => {
|
|
const branches = await getBranches(name)
|
|
const engineToBranches: EngineToBranches = {
|
|
onnx: [],
|
|
gguf: [],
|
|
tensorrtllm: [],
|
|
}
|
|
|
|
for (const branch of branches) {
|
|
if (branch.includes('onnx')) {
|
|
const fileSize = await getFileSizeByRepoAndBranch(name, branch)
|
|
engineToBranches.onnx.push({
|
|
name: branch,
|
|
fileSize: fileSize,
|
|
})
|
|
continue
|
|
}
|
|
if (branch.includes('gguf')) {
|
|
const fileSize = await getFileSizeByRepoAndBranch(name, branch)
|
|
engineToBranches.gguf.push({
|
|
name: branch,
|
|
fileSize: fileSize,
|
|
})
|
|
continue
|
|
}
|
|
if (branch.includes('tensorrt-llm')) {
|
|
const fileSize = await getFileSizeByRepoAndBranch(name, branch)
|
|
engineToBranches.tensorrtllm.push({
|
|
name: branch,
|
|
fileSize: fileSize,
|
|
})
|
|
continue
|
|
}
|
|
}
|
|
|
|
return engineToBranches
|
|
}
|
|
|
|
export const tryGettingReadMeFile = async (
|
|
repoName: string,
|
|
branch?: string
|
|
): Promise<string | undefined> => {
|
|
const revision = branch ?? 'main'
|
|
try {
|
|
for await (const fileInfo of listFiles({
|
|
repo: { type: 'model', name: repoName },
|
|
revision,
|
|
})) {
|
|
if (fileInfo.path !== 'README.md') continue
|
|
|
|
const data = await (
|
|
await downloadFile({
|
|
repo: repoName,
|
|
revision,
|
|
path: fileInfo.path,
|
|
})
|
|
)?.text()
|
|
|
|
if (!data) return undefined
|
|
return markdownParser.parse(data)
|
|
}
|
|
} catch (e) {
|
|
console.debug('Cannot get ReadMe for', repoName, e)
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
const toHuggingFaceUrl = (repoId: string): string => {
|
|
try {
|
|
const url = new URL(repoId)
|
|
if (url.host !== 'huggingface.co') {
|
|
throw new Error(`Invalid Hugging Face repo URL: ${repoId}`)
|
|
}
|
|
|
|
const paths = url.pathname.split('/').filter((e) => e.trim().length > 0)
|
|
if (paths.length < 2) {
|
|
throw new Error(`Invalid Hugging Face repo URL: ${repoId}`)
|
|
}
|
|
|
|
return `${url.origin}/api/models/${paths[0]}/${paths[1]}`
|
|
} catch (err) {
|
|
if (repoId.startsWith('https')) {
|
|
throw new Error(`Cannot parse url: ${repoId}`)
|
|
}
|
|
|
|
return `https://huggingface.co/api/models/${repoId}`
|
|
}
|
|
}
|
|
|
|
export const getFileSize = async (url: string) => {
|
|
try {
|
|
const response = await fetch(url, { method: 'HEAD' })
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`)
|
|
}
|
|
|
|
const contentLength = response.headers.get('Content-Length')
|
|
|
|
if (contentLength) {
|
|
return parseInt(contentLength, 10)
|
|
} else {
|
|
throw new Error('Content-Length header is missing')
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching file size:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
export const fetchHuggingFaceRepoData = async (
|
|
repoId: string
|
|
): Promise<HuggingFaceRepoData> => {
|
|
const sanitizedUrl = toHuggingFaceUrl(repoId)
|
|
console.debug('sanitizedUrl', sanitizedUrl)
|
|
|
|
const headers = {
|
|
Accept: 'application/json',
|
|
}
|
|
|
|
const res = await fetch(sanitizedUrl, {
|
|
headers: headers,
|
|
})
|
|
const response = await res.json()
|
|
if (response['error'] != null) {
|
|
throw new Error(response['error'])
|
|
}
|
|
|
|
const data = response as HuggingFaceRepoData
|
|
|
|
if (data.tags.indexOf('gguf') === -1) {
|
|
throw new Error(
|
|
`${repoId} is not supported. Only GGUF models are supported.`
|
|
)
|
|
}
|
|
|
|
const url = new URL(sanitizedUrl)
|
|
const paths = url.pathname.split('/').filter((e) => e.trim().length > 0)
|
|
const sanitizedRepoId = `${paths[2]}/${paths[3]}`
|
|
|
|
for (const sibling of data.siblings) {
|
|
const downloadUrl = `https://huggingface.co/${sanitizedRepoId}/resolve/main/${sibling.rfilename}`
|
|
sibling.downloadUrl = downloadUrl
|
|
}
|
|
for await (const fileInfo of listFiles({
|
|
repo: { type: 'model', name: sanitizedRepoId },
|
|
})) {
|
|
const sibling = data.siblings.find(
|
|
(sibling) => sibling.rfilename === fileInfo.path
|
|
)
|
|
if (sibling) {
|
|
sibling.fileSize = fileInfo.size
|
|
}
|
|
}
|
|
|
|
AllQuantizations.forEach((quantization) => {
|
|
data.siblings.forEach((sibling) => {
|
|
if (!sibling.quantization && sibling.rfilename.includes(quantization)) {
|
|
sibling.quantization = quantization
|
|
}
|
|
})
|
|
})
|
|
data.siblings = data.siblings
|
|
.filter((sibling) => sibling.quantization != null)
|
|
.filter((sibling) => sibling.fileSize != null)
|
|
|
|
data.modelUrl = `https://huggingface.co/${paths[2]}/${paths[3]}`
|
|
return data
|
|
}
|
|
|
|
// TODO: move this to somewhere else
|
|
export interface HfModelEntry extends ModelEntry {
|
|
model?: Model // TODO: deprecated this
|
|
remoteModel?: boolean
|
|
engine?: LlmEngine
|
|
}
|
|
|
|
export type EngineType = 'onnx' | 'gguf' | 'tensorrtllm'
|
|
|
|
export type EngineToBranches = Record<EngineType, CortexHubModel[]>
|
|
|
|
export type CortexHubModel = {
|
|
name: string
|
|
fileSize?: number
|
|
}
|