99 */
1010
1111import { parse } from "https://deno.land/std@0.224.0/flags/mod.ts" ;
12- import * as yaml from "https://deno.land/std @0.224 .0/yaml/mod.ts " ;
12+ import { hf2catalog } from "jsr:@arcfra/neutree-mcp-servers @0.3 .0/servers/hf2catalog/hf2catalog " ;
1313
14- // -----------------------------
15- // Types & Constants
16- // -----------------------------
17- interface SiblingFile {
18- rfilename : string ; // full relative filename in repo
19- size : number ;
20- }
21-
22- interface HFModelMeta {
23- // subset of HF /api/models response we care about
24- pipeline_tag ?: string ;
25- siblings : SiblingFile [ ] ;
26- id ?: string ; // full model name like "microsoft/DialoGPT-medium"
27- author ?: string ; // author/organization name
28- cardData ?: {
29- thumbnail ?: string ; // icon/thumbnail URL
30- } ;
31- }
32-
33- const DEFAULT_SCHEDULER = {
34- type : "consistent_hash" ,
35- virtual_nodes : 150 ,
36- load_factor : 1.25 ,
37- } ;
38-
39- // Supported model tasks
40- const TEXT_GENERATION_TASK = "text-generation" ;
41- const TEXT_EMBEDDING_TASK = "text-embedding" ;
42- const TEXT_RERANK_TASK = "text-rerank" ;
43-
44- const SUPPORTED_TASKS = [
45- TEXT_GENERATION_TASK ,
46- TEXT_EMBEDDING_TASK ,
47- TEXT_RERANK_TASK ,
48- ] as const ;
49-
50- // Mapping from HuggingFace pipeline tags to our supported tasks
51- // Keep this mapping conservative - only include well-established mappings
52- const HF_PIPELINE_TO_TASK_MAP : Record < string , string > = {
53- // Text generation - only the most common ones
54- "text-generation" : TEXT_GENERATION_TASK ,
55-
56- // Text embedding - only exact matches and feature-extraction (widely used)
57- "feature-extraction" : TEXT_EMBEDDING_TASK ,
58- "text-embedding" : TEXT_EMBEDDING_TASK ,
59- "sentence-similarity" : TEXT_EMBEDDING_TASK ,
60-
61- // Text rerank - only exact match
62- "text-rerank" : TEXT_RERANK_TASK ,
63- } ;
64-
65- // vLLM best-practice args (can be tuned globally here)
66- const DEFAULT_VLLM_ARGS = {
67- tensor_parallel_size : 1 ,
68- max_model_len : 32768 ,
69- enforce_eager : true ,
70- gpu_memory_utilization : 0.95 ,
71- enable_chunked_prefill : true ,
72- } ;
73-
74- // -----------------------------
75- // Helpers
76- // -----------------------------
77- function fatal ( msg : string ) : never {
78- console . error ( `Error: ${ msg } ` ) ;
79- Deno . exit ( 1 ) ;
80- }
81-
82- function mapHFPipelineToTask ( pipelineTag ?: string ) : string {
83- if ( ! pipelineTag ) {
84- fatal ( "Model pipeline_tag is missing. Cannot determine task type." ) ;
85- }
86-
87- const mappedTask = HF_PIPELINE_TO_TASK_MAP [ pipelineTag ] ;
88- if ( ! mappedTask ) {
89- const supportedPipelines = Object . keys ( HF_PIPELINE_TO_TASK_MAP ) . join ( ", " ) ;
90- fatal (
91- `Unsupported pipeline tag: "${ pipelineTag } ". ` +
92- `Supported pipeline tags: ${ supportedPipelines } . ` +
93- `Only tasks [${ SUPPORTED_TASKS . join ( ", " ) } ] are supported.`
94- ) ;
95- }
96-
97- return mappedTask ;
98- }
99-
100- function parseRepoUrl ( url : string ) : { owner : string ; repo : string } {
101- try {
102- const u = new URL ( url . replace ( / \/ $ / , "" ) ) ;
103- if ( u . hostname !== "huggingface.co" ) {
104- throw new Error ( "Not a huggingface.co URL" ) ;
105- }
106- const segments = u . pathname . replace ( / ^ \/ + / , "" ) . split ( "/" ) ;
107- if ( segments . length < 2 ) throw new Error ( "URL missing owner or repo" ) ;
108- return { owner : segments [ 0 ] , repo : segments [ 1 ] } ;
109- } catch ( e : unknown ) {
110- fatal ( ( e as Error ) . message ) ;
111- }
112- }
113-
114- async function fetchModelMeta (
115- owner : string ,
116- repo : string
117- ) : Promise < HFModelMeta > {
118- const apiUrl = `https://huggingface.co/api/models/${ owner } /${ repo } ` ;
119- const res = await fetch ( apiUrl ) ;
120- if ( ! res . ok ) fatal ( `HF API request failed: ${ res . status } ${ res . statusText } ` ) ;
121- return await res . json ( ) ;
122- }
123-
124- function pickFile ( siblings : SiblingFile [ ] , ext : string ) : string | undefined {
125- return siblings . find ( ( s ) => s . rfilename . endsWith ( ext ) ) ?. rfilename ;
126- }
127-
128- function selectPrimaryFile ( meta : HFModelMeta ) : {
129- engine : "vllm" | "llama-cpp" ;
130- file : string ;
131- } {
132- const ggufFile = pickFile ( meta . siblings , ".gguf" ) ;
133- if ( ggufFile ) {
134- return { engine : "llama-cpp" , file : ggufFile } ;
135- }
136-
137- // Prefer first shard of a safetensors split, else any safetensors
138- const firstShard = meta . siblings . find (
139- ( s ) => / \. s a f e t e n s o r s $ / . test ( s . rfilename ) && / - 0 0 0 0 1 - o f - / . test ( s . rfilename )
140- ) ;
141- if ( firstShard ) return { engine : "vllm" , file : firstShard . rfilename } ;
142- const anyST = pickFile ( meta . siblings , ".safetensors" ) ;
143- if ( anyST ) return { engine : "vllm" , file : anyST } ;
144-
145- fatal ( "No .gguf or .safetensors file found in repo" ) ;
146- }
147-
148- function slugifyName ( repo : string ) : string {
149- return repo
150- . toLowerCase ( )
151- . replace ( / [ ^ a - z 0 - 9 ] + / g, "-" )
152- . replace ( / ( ^ - | - $ ) / g, "" ) ;
153- }
154-
155- function buildDisplayName ( meta : HFModelMeta , repo : string ) : string {
156- // Try to use the model ID as display name, fallback to repo name
157- if ( meta . id ) {
158- return meta . id ;
159- }
160- return repo ;
161- }
162-
163- function buildLabels ( meta : HFModelMeta , owner : string , repo : string ) : Record < string , string > {
164- const labels : Record < string , string > = { } ;
165-
166- // Priority 1: Use thumbnail from cardData if available
167- if ( meta . cardData ?. thumbnail ) {
168- labels . icon_url = meta . cardData . thumbnail ;
169- } else {
170- // Priority 2: Use HuggingFace social thumbnail for the organization/user
171- // This provides high-quality avatars for HF organizations
172- labels . icon_url = `https://cdn-thumbnails.huggingface.co/social-thumbnails/${ owner } .png` ;
173- }
174-
175- // Add original HuggingFace repo URL for traceability
176- labels . hf_repo_url = `https://huggingface.co/${ owner } /${ repo } ` ;
177-
178- return labels ;
179- }
180-
181- function buildCatalog (
182- { owner, repo } : { owner : string ; repo : string } ,
183- meta : HFModelMeta
184- ) {
185- const { engine, file } = selectPrimaryFile ( meta ) ;
186- const task = mapHFPipelineToTask ( meta . pipeline_tag ) ;
187-
188- // metadata.name uses repo slug; workspace omitted
189- const name = slugifyName ( repo ) ;
190- const displayName = buildDisplayName ( meta , repo ) ;
191- const labels = buildLabels ( meta , owner , repo ) ;
192-
193- const catalog : Record < string , unknown > = {
194- apiVersion : "v1" ,
195- kind : "ModelCatalog" ,
196- metadata : {
197- name,
198- display_name : displayName ,
199- // workspace intentionally left blank for UI to fill
200- labels,
201- } ,
202- spec : {
203- model : {
204- registry : "" ,
205- name : `${ owner } /${ repo } ` ,
206- file,
207- version : "latest" ,
208- task : task ,
209- } ,
210- engine : {
211- engine,
212- version : "v1" ,
213- } ,
214- resources : { } ,
215- replicas : { num : 1 } ,
216- deployment_options : {
217- scheduler : DEFAULT_SCHEDULER ,
218- } ,
219- variables : {
220- RAY_SCHEDULER_TYPE : DEFAULT_SCHEDULER . type ,
221- ...( engine === "vllm"
222- ? {
223- engine_args : {
224- ...DEFAULT_VLLM_ARGS ,
225- served_model_name : `${ owner } /${ repo } ` ,
226- } ,
227- }
228- : { } ) ,
229- } ,
230- } ,
231- } ;
232-
233- return catalog ;
234- }
235-
236- // -----------------------------
237- // Main
238- // -----------------------------
23914async function main ( ) {
24015 const {
24116 _ : [ repoUrl ] ,
@@ -245,18 +20,12 @@ async function main() {
24520 alias : { j : "json" } ,
24621 } ) ;
24722
248- if ( ! repoUrl || typeof repoUrl !== "string" )
249- fatal ( "Please provide a Hugging Face repo URL." ) ;
250-
251- const { owner, repo } = parseRepoUrl ( repoUrl ) ;
252- const meta = await fetchModelMeta ( owner , repo ) ;
253- const catalog = buildCatalog ( { owner, repo } , meta ) ;
254-
255- if ( jsonFlag ) {
256- console . log ( JSON . stringify ( catalog , null , 2 ) ) ;
257- } else {
258- console . log ( yaml . stringify ( catalog ) ) ;
259- }
23+ console . log (
24+ await hf2catalog ( {
25+ repoUrl : String ( repoUrl ) ,
26+ output : jsonFlag ? "json" : "yaml" ,
27+ } )
28+ ) ;
26029}
26130
26231if ( import . meta. main ) main ( ) ;
0 commit comments