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

Commit 0131f79

Browse files
committed
feat: pull model from jan hub
1 parent d31a788 commit 0131f79

File tree

5 files changed

+98
-41
lines changed

5 files changed

+98
-41
lines changed

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

Lines changed: 86 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,9 @@ export class ModelsCliUsecases {
9999
}
100100

101101
async pullModel(modelId: string) {
102-
if (modelId.includes('/')) {
102+
if (modelId.includes('/') || modelId.includes(':')) {
103103
await this.pullHuggingFaceModel(modelId);
104104
}
105-
106105
const bar = new SingleBar({}, Presets.shades_classic);
107106
bar.start(100, 0);
108107
const callback = (progress: number) => {
@@ -112,20 +111,35 @@ export class ModelsCliUsecases {
112111
}
113112

114113
private async pullHuggingFaceModel(modelId: string) {
115-
const data = await this.fetchHuggingFaceRepoData(modelId);
116-
const { quantization } = await this.inquirerService.inquirer.prompt({
117-
type: 'list',
118-
name: 'quantization',
119-
message: 'Select quantization',
120-
choices: data.siblings
121-
.map((e) => e.quantization)
122-
.filter((e) => e != null),
123-
});
124-
125-
const sibling = data.siblings
126-
.filter((e) => !!e.quantization)
127-
.find((e: any) => e.quantization === quantization);
114+
let data: HuggingFaceRepoData;
115+
if (modelId.includes('/'))
116+
data = await this.fetchHuggingFaceRepoData(modelId);
117+
else data = await this.fetchJanRepoData(modelId);
118+
119+
let sibling;
120+
121+
const listChoices = data.siblings
122+
.filter((e) => e.quantization != null)
123+
.map((e) => {
124+
return {
125+
name: e.quantization,
126+
value: e.quantization,
127+
};
128+
});
128129

130+
if (listChoices.length > 1) {
131+
const { quantization } = await this.inquirerService.inquirer.prompt({
132+
type: 'list',
133+
name: 'quantization',
134+
message: 'Select quantization',
135+
choices: listChoices,
136+
});
137+
sibling = data.siblings
138+
.filter((e) => !!e.quantization)
139+
.find((e: any) => e.quantization === quantization);
140+
} else {
141+
sibling = data.siblings.find((e) => e.rfilename.includes('.gguf'));
142+
}
129143
if (!sibling) throw 'No expected quantization found';
130144

131145
let stopWord = '';
@@ -141,9 +155,7 @@ export class ModelsCliUsecases {
141155

142156
// @ts-expect-error "tokenizer.ggml.tokens"
143157
stopWord = metadata['tokenizer.ggml.tokens'][index] ?? '';
144-
} catch (err) {
145-
console.log('Failed to get stop word: ', err);
146-
}
158+
} catch (err) {}
147159

148160
const stopWords: string[] = [];
149161
if (stopWord.length > 0) {
@@ -163,6 +175,7 @@ export class ModelsCliUsecases {
163175
description: '',
164176
settings: {
165177
prompt_template: promptTemplate,
178+
llama_model_path: sibling.rfilename,
166179
},
167180
parameters: {
168181
stop: stopWords,
@@ -209,6 +222,59 @@ export class ModelsCliUsecases {
209222
}
210223
}
211224

225+
private async fetchJanRepoData(modelId: string) {
226+
const repo = modelId.split(':')[0];
227+
const tree = modelId.split(':')[1];
228+
const url = this.toHuggingFaceUrl(`janhq/${repo}`, tree);
229+
const res = await fetch(url);
230+
const response:
231+
| {
232+
path: string;
233+
size: number;
234+
}[]
235+
| { error: string } = await res.json();
236+
237+
if ('error' in response && response.error != null) {
238+
throw new Error(response.error);
239+
}
240+
241+
const data: HuggingFaceRepoData = {
242+
siblings: Array.isArray(response)
243+
? response.map((e) => {
244+
return {
245+
rfilename: e.path,
246+
downloadUrl: `https://huggingface.co/janhq/${repo}/resolve/${tree}/${e.path}`,
247+
fileSize: e.size ?? 0,
248+
};
249+
})
250+
: [],
251+
tags: ['gguf'],
252+
id: modelId,
253+
modelId: modelId,
254+
author: 'janhq',
255+
sha: '',
256+
downloads: 0,
257+
lastModified: '',
258+
private: false,
259+
disabled: false,
260+
gated: false,
261+
pipeline_tag: 'text-generation',
262+
cardData: {},
263+
createdAt: '',
264+
};
265+
266+
AllQuantizations.forEach((quantization) => {
267+
data.siblings.forEach((sibling: any) => {
268+
if (!sibling.quantization && sibling.rfilename.includes(quantization)) {
269+
sibling.quantization = quantization;
270+
}
271+
});
272+
});
273+
274+
data.modelUrl = url;
275+
return data;
276+
}
277+
212278
private async fetchHuggingFaceRepoData(repoId: string) {
213279
const sanitizedUrl = this.toHuggingFaceUrl(repoId);
214280

@@ -245,24 +311,7 @@ export class ModelsCliUsecases {
245311
return data;
246312
}
247313

248-
private toHuggingFaceUrl(repoId: string): string {
249-
try {
250-
const url = new URL(`https://huggingface.co/${repoId}`);
251-
if (url.host !== 'huggingface.co') {
252-
throw `Invalid Hugging Face repo URL: ${repoId}`;
253-
}
254-
255-
const paths = url.pathname.split('/').filter((e) => e.trim().length > 0);
256-
if (paths.length < 2) {
257-
throw `Invalid Hugging Face repo URL: ${repoId}`;
258-
}
259-
260-
return `${url.origin}/api/models/${paths[0]}/${paths[1]}`;
261-
} catch (err) {
262-
if (repoId.startsWith('https')) {
263-
throw new Error(`Cannot parse url: ${repoId}`);
264-
}
265-
throw err;
266-
}
314+
private toHuggingFaceUrl(repoId: string, tree?: string): string {
315+
return `https://huggingface.co/api/models/${repoId}${tree ? `/tree/${tree}` : ''}`;
267316
}
268317
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const normalizeModelId = (modelId: string): string => {
2+
return modelId.replace(':', '%3A');
3+
};

cortex-js/src/infrastructure/providers/cortex/cortex.provider.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Model, ModelSettingParams } from '@/domain/models/model.interface';
66
import { HttpService } from '@nestjs/axios';
77
import { defaultCortexCppHost, defaultCortexCppPort } from 'constant';
88
import { readdirSync } from 'node:fs';
9+
import { normalizeModelId } from '@/infrastructure/commanders/utils/normalize-model-id';
910

1011
/**
1112
* A class that implements the InferenceExtension interface from the @janhq/core package.
@@ -32,7 +33,10 @@ export default class CortexProvider extends OAIEngineExtension {
3233
): Promise<void> {
3334
const modelsContainerDir = this.modelDir();
3435

35-
const modelFolderFullPath = join(modelsContainerDir, model.id);
36+
const modelFolderFullPath = join(
37+
modelsContainerDir,
38+
normalizeModelId(model.id),
39+
);
3640
const ggufFiles = readdirSync(modelFolderFullPath).filter((file) => {
3741
return file.endsWith('.gguf');
3842
});

cortex-js/src/usecases/cortex/cortex.usecases.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class CortexUsecases {
3232
);
3333

3434
if (!existsSync(cortexCppPath)) {
35-
throw new Error('Cortex binary not found');
35+
throw new Error('The engine is not available, please run "cortex init".');
3636
}
3737

3838
// go up one level to get the binary folder, have to also work on windows

cortex-js/src/usecases/models/models.usecases.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { ExtensionRepository } from '@/domain/repositories/extension.interface';
2323
import { EngineExtension } from '@/domain/abstracts/engine.abstract';
2424
import { HttpService } from '@nestjs/axios';
2525
import { ModelSettingParamsDto } from '@/infrastructure/dtos/models/model-setting-params.dto';
26+
import { normalizeModelId } from '@/infrastructure/commanders/utils/normalize-model-id';
2627

2728
@Injectable()
2829
export class ModelsUsecases {
@@ -106,7 +107,7 @@ export class ModelsUsecases {
106107
return;
107108
}
108109

109-
const modelFolder = join(modelsContainerDir, id);
110+
const modelFolder = join(modelsContainerDir, normalizeModelId(id));
110111

111112
return this.modelRepository
112113
.delete(id)
@@ -205,7 +206,7 @@ export class ModelsUsecases {
205206
mkdirSync(modelsContainerDir, { recursive: true });
206207
}
207208

208-
const modelFolder = join(modelsContainerDir, model.id);
209+
const modelFolder = join(modelsContainerDir, normalizeModelId(model.id));
209210
await promises.mkdir(modelFolder, { recursive: true });
210211
const destination = join(modelFolder, fileName);
211212

0 commit comments

Comments
 (0)