jan/web/utils/huggingface.ts
NamH 101268f6f3
feat: integrating cortex (#3001)
* 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>
2024-07-26 17:52:43 +07:00

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
}