Skip to content
This repository was archived by the owner on Jul 4, 2025. It is now read-only.

Commit 707a24a

Browse files
committed
feat: pull model yaml from hf
1 parent 7e46c2a commit 707a24a

File tree

3 files changed

+167
-4
lines changed

3 files changed

+167
-4
lines changed

cortex-js/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
},
2727
"dependencies": {
2828
"@huggingface/gguf": "^0.1.5",
29+
"@huggingface/hub": "^0.15.1",
2930
"@nestjs/axios": "^3.0.2",
3031
"@nestjs/common": "^10.0.0",
3132
"@nestjs/config": "^3.2.2",
@@ -47,7 +48,8 @@
4748
"sqlite": "^5.1.1",
4849
"sqlite3": "^5.1.7",
4950
"typeorm": "^0.3.20",
50-
"ulid": "^2.3.0"
51+
"ulid": "^2.3.0",
52+
"yaml": "^2.4.2"
5153
},
5254
"devDependencies": {
5355
"@nestjs/cli": "^10.0.0",
Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,151 @@
11
import { CommandRunner, SubCommand } from 'nest-commander';
2-
import { exit } from 'node:process';
2+
import { exit, stdin, stdout } from 'node:process';
33
import { ModelsCliUsecases } from '../usecases/models.cli.usecases';
4+
import { RepoDesignation, listFiles } from '@huggingface/hub';
5+
import YAML from 'yaml';
6+
import * as readline from 'node:readline/promises';
7+
import { basename } from 'node:path';
48

59
@SubCommand({
610
name: 'pull',
711
aliases: ['download'],
812
description: 'Download a model. Working with HuggingFace model id.',
913
})
1014
export class ModelPullCommand extends CommandRunner {
15+
private metadataFileName = 'metadata.yaml';
16+
1117
constructor(private readonly modelsCliUsecases: ModelsCliUsecases) {
1218
super();
1319
}
1420

1521
async run(input: string[]) {
1622
if (input.length < 1) {
17-
console.error('Model ID is required');
23+
console.error('Model Id is required');
1824
exit(1);
1925
}
2026

21-
await this.modelsCliUsecases.pullModel(input[0]);
27+
// Check if metadata.yaml file exist
28+
const metadata = await this.getJanMetadata(input[0]);
29+
30+
if (!metadata) {
31+
await this.modelsCliUsecases.pullModel(input[0]);
32+
} else {
33+
await this.handleJanHqModel(input[0], metadata);
34+
}
35+
2236
console.log('\nDownload complete!');
2337
exit(0);
2438
}
39+
40+
private async getJanMetadata(input: string): Promise<any> {
41+
// try to append with janhq/ if it's not already
42+
const sanitizedInput = input.trim().startsWith('janhq/')
43+
? input
44+
: `janhq/${input}`;
45+
46+
const repo: RepoDesignation = { type: 'model', name: sanitizedInput };
47+
let isMetadataFileExist = false;
48+
for await (const fileInfo of listFiles({ repo })) {
49+
if (fileInfo.path === this.metadataFileName) {
50+
isMetadataFileExist = true;
51+
break;
52+
}
53+
}
54+
55+
if (!isMetadataFileExist) {
56+
return undefined;
57+
}
58+
59+
const path = `https://huggingface.co/${sanitizedInput}/raw/main/${this.metadataFileName}`;
60+
const res = await fetch(path);
61+
const metadataJson = await res.text();
62+
const parsedMetadata = YAML.parse(metadataJson);
63+
return parsedMetadata;
64+
}
65+
66+
private async versionInquiry(tags: string[]): Promise<string> {
67+
return new Promise((resolve) => {
68+
let selectedTag = 'default';
69+
let prompt = 'Select the version you want to download:\n';
70+
for (let i = 0; i < tags.length; i++) {
71+
prompt += `${i}. ${tags[i]}\n`;
72+
}
73+
prompt += '>> ';
74+
75+
const rl = readline.createInterface({
76+
input: stdin,
77+
output: stdout,
78+
prompt: prompt,
79+
});
80+
rl.prompt();
81+
82+
rl.on('close', () => {
83+
resolve(selectedTag);
84+
});
85+
86+
rl.on('line', (input) => {
87+
if (input.trim().length === 0) {
88+
rl.close();
89+
}
90+
91+
try {
92+
if (Number(input) >= 0 && Number(input) < tags.length) {
93+
selectedTag = tags[Number(input)];
94+
rl.close();
95+
} else {
96+
console.error('Invalid version');
97+
rl.prompt();
98+
}
99+
} catch (e) {
100+
console.error('Invalid version');
101+
rl.prompt();
102+
}
103+
});
104+
});
105+
}
106+
107+
private async handleJanHqModel(repoName: string, metadata: any) {
108+
const sanitizedRepoName = repoName.trim().startsWith('janhq/')
109+
? repoName
110+
: `janhq/${repoName}`;
111+
112+
const tags = metadata.tags;
113+
let selectedTag = 'default';
114+
const allTags: string[] = Object.keys(tags);
115+
116+
if (allTags.length > 1) {
117+
selectedTag = await this.versionInquiry(allTags);
118+
}
119+
120+
const branch = selectedTag;
121+
const engine = 'llamacpp'; // TODO: currently, we only support llamacpp
122+
123+
const revision = metadata.tags?.[branch]?.[engine];
124+
if (!revision) {
125+
console.error("Can't find model revision.");
126+
exit(1);
127+
}
128+
129+
const repo: RepoDesignation = { type: 'model', name: sanitizedRepoName };
130+
let ggufUrl: string | undefined = undefined;
131+
for await (const fileInfo of listFiles({
132+
repo: repo,
133+
revision: revision,
134+
})) {
135+
if (fileInfo.path.endsWith('.gguf')) {
136+
ggufUrl = `https://huggingface.co/${sanitizedRepoName}/resolve/${revision}/${fileInfo.path}`;
137+
break;
138+
}
139+
}
140+
141+
if (!ggufUrl) {
142+
console.error("Can't find model file.");
143+
exit(1);
144+
}
145+
console.log('Downloading', basename(ggufUrl));
146+
await this.modelsCliUsecases.pullModelWithExactUrl(
147+
`${sanitizedRepoName}/${revision}`,
148+
ggufUrl,
149+
);
150+
}
25151
}

cortex-js/src/infrastructure/commanders/usecases/models.cli.usecases.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,41 @@ export class ModelsCliUsecases {
9898
return this.modelsUsecases.remove(modelId);
9999
}
100100

101+
async pullModelWithExactUrl(modelId: string, url: string) {
102+
const model: CreateModelDto = {
103+
sources: [
104+
{
105+
url: url,
106+
},
107+
],
108+
id: modelId,
109+
name: modelId,
110+
version: '', // TODO: get version from the file
111+
format: ModelFormat.GGUF,
112+
description: '',
113+
settings: {
114+
prompt_template: '', // TODO: get prompt template from the file
115+
},
116+
parameters: {
117+
stop: [],
118+
},
119+
metadata: {
120+
author: 'janhq', // TODO: get author from the file
121+
size: 0, // TODO: get size from the file
122+
tags: [],
123+
},
124+
engine: 'cortex',
125+
};
126+
if (!(await this.modelsUsecases.findOne(modelId)))
127+
await this.modelsUsecases.create(model);
128+
const bar = new SingleBar({}, Presets.shades_classic);
129+
bar.start(100, 0);
130+
const callback = (progress: number) => {
131+
bar.update(progress);
132+
};
133+
await this.modelsUsecases.downloadModel(modelId, callback);
134+
}
135+
101136
async pullModel(modelId: string) {
102137
if (modelId.includes('/')) {
103138
await this.pullHuggingFaceModel(modelId);

0 commit comments

Comments
 (0)