@fugood/bricks-project 2.23.3 → 2.23.5-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/compile/action-name-map.ts +14 -0
  2. package/compile/index.ts +29 -0
  3. package/package.json +3 -3
  4. package/skills/bricks-project/rules/architecture-patterns.md +7 -0
  5. package/skills/bricks-project/rules/buttress.md +9 -6
  6. package/tools/mcp-server.ts +10 -880
  7. package/tools/mcp-tools/compile.ts +91 -0
  8. package/tools/mcp-tools/huggingface.ts +763 -0
  9. package/tools/mcp-tools/icons.ts +60 -0
  10. package/tools/mcp-tools/lottie.ts +102 -0
  11. package/tools/mcp-tools/media.ts +110 -0
  12. package/tools/postinstall.ts +113 -28
  13. package/tools/pull.ts +25 -9
  14. package/types/animation.ts +4 -0
  15. package/types/automation.ts +3 -0
  16. package/types/bricks/Camera.ts +33 -6
  17. package/types/bricks/GenerativeMedia.ts +5 -5
  18. package/types/bricks/Icon.ts +2 -2
  19. package/types/bricks/Image.ts +3 -3
  20. package/types/bricks/Items.ts +6 -6
  21. package/types/bricks/Lottie.ts +3 -3
  22. package/types/bricks/Maps.ts +3 -3
  23. package/types/bricks/QrCode.ts +3 -3
  24. package/types/bricks/Rect.ts +3 -3
  25. package/types/bricks/RichText.ts +2 -2
  26. package/types/bricks/Slideshow.ts +1 -1
  27. package/types/bricks/Svg.ts +2 -2
  28. package/types/bricks/Text.ts +3 -3
  29. package/types/bricks/TextInput.ts +10 -6
  30. package/types/bricks/Video.ts +3 -3
  31. package/types/bricks/VideoStreaming.ts +2 -2
  32. package/types/bricks/WebView.ts +3 -3
  33. package/types/canvas.ts +2 -0
  34. package/types/common.ts +5 -0
  35. package/types/data-calc-command.ts +2 -0
  36. package/types/data-calc.ts +1 -0
  37. package/types/data.ts +2 -0
  38. package/types/generators/AlarmClock.ts +4 -4
  39. package/types/generators/Assistant.ts +53 -8
  40. package/types/generators/BleCentral.ts +11 -3
  41. package/types/generators/BlePeripheral.ts +3 -3
  42. package/types/generators/CanvasMap.ts +3 -3
  43. package/types/generators/CastlesPay.ts +2 -2
  44. package/types/generators/DataBank.ts +29 -2
  45. package/types/generators/File.ts +62 -13
  46. package/types/generators/GraphQl.ts +2 -2
  47. package/types/generators/Http.ts +25 -6
  48. package/types/generators/HttpServer.ts +4 -4
  49. package/types/generators/Information.ts +1 -1
  50. package/types/generators/Intent.ts +7 -1
  51. package/types/generators/Iterator.ts +5 -5
  52. package/types/generators/Keyboard.ts +15 -5
  53. package/types/generators/LlmAnthropicCompat.ts +9 -3
  54. package/types/generators/LlmAppleBuiltin.ts +4 -4
  55. package/types/generators/LlmGgml.ts +63 -13
  56. package/types/generators/LlmMlx.ts +210 -0
  57. package/types/generators/LlmOnnx.ts +13 -4
  58. package/types/generators/LlmOpenAiCompat.ts +19 -3
  59. package/types/generators/LlmQualcommAiEngine.ts +29 -5
  60. package/types/generators/Mcp.ts +331 -16
  61. package/types/generators/McpServer.ts +34 -7
  62. package/types/generators/MediaFlow.ts +24 -6
  63. package/types/generators/MqttBroker.ts +9 -3
  64. package/types/generators/MqttClient.ts +10 -4
  65. package/types/generators/Question.ts +4 -4
  66. package/types/generators/RealtimeTranscription.ts +81 -10
  67. package/types/generators/RerankerGgml.ts +19 -5
  68. package/types/generators/SerialPort.ts +5 -5
  69. package/types/generators/SoundPlayer.ts +1 -1
  70. package/types/generators/SoundRecorder.ts +16 -5
  71. package/types/generators/SpeechToTextGgml.ts +27 -7
  72. package/types/generators/SpeechToTextOnnx.ts +3 -3
  73. package/types/generators/SpeechToTextPlatform.ts +3 -3
  74. package/types/generators/SqLite.ts +9 -5
  75. package/types/generators/Step.ts +2 -2
  76. package/types/generators/SttAppleBuiltin.ts +4 -4
  77. package/types/generators/Tcp.ts +3 -3
  78. package/types/generators/TcpServer.ts +5 -5
  79. package/types/generators/TextToSpeechAppleBuiltin.ts +3 -3
  80. package/types/generators/TextToSpeechGgml.ts +3 -3
  81. package/types/generators/TextToSpeechOnnx.ts +3 -3
  82. package/types/generators/TextToSpeechOpenAiLike.ts +3 -3
  83. package/types/generators/ThermalPrinter.ts +4 -4
  84. package/types/generators/Tick.ts +2 -2
  85. package/types/generators/Udp.ts +8 -3
  86. package/types/generators/VadGgml.ts +34 -5
  87. package/types/generators/VadOnnx.ts +27 -4
  88. package/types/generators/VadTraditional.ts +13 -7
  89. package/types/generators/VectorStore.ts +22 -5
  90. package/types/generators/Watchdog.ts +10 -5
  91. package/types/generators/WebCrawler.ts +3 -3
  92. package/types/generators/WebRtc.ts +14 -8
  93. package/types/generators/WebSocket.ts +4 -4
  94. package/types/generators/index.ts +1 -0
  95. package/types/subspace.ts +1 -0
  96. package/types/system.ts +1 -1
  97. package/utils/event-props.ts +104 -87
@@ -0,0 +1,763 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { z } from 'zod'
3
+ import { JSON5 } from 'bun'
4
+ import * as TOON from '@toon-format/toon'
5
+ import { gguf } from '@huggingface/gguf'
6
+
7
+ // Hugging Face API configuration
8
+ const HF_API_URL = 'https://huggingface.co/api'
9
+ const { HF_TOKEN } = process.env
10
+
11
+ // Helper function to convert BigInt to number for JSON serialization
12
+ const convertBigIntToNumber = (value: unknown): number | unknown => {
13
+ if (typeof value === 'bigint') {
14
+ return Number(value)
15
+ }
16
+ return value
17
+ }
18
+
19
+ // Extract GGUF metadata from a GGUF file URL with 10s timeout
20
+ const extractGGUFMetadata = async (url: string) => {
21
+ const timeoutPromise = new Promise<never>((_, reject) =>
22
+ setTimeout(() => reject(new Error('GGUF metadata extraction timeout')), 10000),
23
+ )
24
+
25
+ try {
26
+ const { metadata } = (await Promise.race([gguf(url), timeoutPromise])) as { metadata: any }
27
+ const architecture = metadata['general.architecture']
28
+
29
+ const ggufSimplifiedMetadata: Record<string, unknown> = {
30
+ name: metadata['general.name'],
31
+ size_label: metadata['general.size_label'],
32
+ basename: metadata['general.basename'],
33
+ architecture: metadata['general.architecture'],
34
+ file_type: convertBigIntToNumber(metadata['general.file_type']),
35
+ quantization_version: convertBigIntToNumber(metadata['general.quantization_version']),
36
+ }
37
+
38
+ if (!architecture) return ggufSimplifiedMetadata
39
+
40
+ // Helper to add converted value if defined
41
+ const addIfDefined = (target: Record<string, unknown>, key: string, value: unknown) => {
42
+ if (value !== undefined) target[key] = convertBigIntToNumber(value)
43
+ }
44
+
45
+ // Architecture-specific transformer parameters
46
+ addIfDefined(ggufSimplifiedMetadata, 'n_ctx_train', metadata[`${architecture}.context_length`])
47
+ addIfDefined(ggufSimplifiedMetadata, 'n_layer', metadata[`${architecture}.block_count`])
48
+ addIfDefined(ggufSimplifiedMetadata, 'n_embd', metadata[`${architecture}.embedding_length`])
49
+ addIfDefined(ggufSimplifiedMetadata, 'n_head', metadata[`${architecture}.attention.head_count`])
50
+ addIfDefined(
51
+ ggufSimplifiedMetadata,
52
+ 'n_head_kv',
53
+ metadata[`${architecture}.attention.head_count_kv`],
54
+ )
55
+ addIfDefined(
56
+ ggufSimplifiedMetadata,
57
+ 'n_embd_head_k',
58
+ metadata[`${architecture}.attention.key_length`],
59
+ )
60
+ addIfDefined(
61
+ ggufSimplifiedMetadata,
62
+ 'n_embd_head_v',
63
+ metadata[`${architecture}.attention.value_length`],
64
+ )
65
+ addIfDefined(
66
+ ggufSimplifiedMetadata,
67
+ 'n_swa',
68
+ metadata[`${architecture}.attention.sliding_window`],
69
+ )
70
+
71
+ // SSM (Mamba) parameters for recurrent/hybrid models
72
+ addIfDefined(ggufSimplifiedMetadata, 'ssm_d_conv', metadata[`${architecture}.ssm.conv_kernel`])
73
+ addIfDefined(ggufSimplifiedMetadata, 'ssm_d_state', metadata[`${architecture}.ssm.state_size`])
74
+ addIfDefined(ggufSimplifiedMetadata, 'ssm_d_inner', metadata[`${architecture}.ssm.inner_size`])
75
+ addIfDefined(ggufSimplifiedMetadata, 'ssm_n_group', metadata[`${architecture}.ssm.group_count`])
76
+ addIfDefined(
77
+ ggufSimplifiedMetadata,
78
+ 'ssm_dt_rank',
79
+ metadata[`${architecture}.ssm.time_step_rank`],
80
+ )
81
+
82
+ // RWKV parameters
83
+ addIfDefined(
84
+ ggufSimplifiedMetadata,
85
+ 'rwkv_head_size',
86
+ metadata[`${architecture}.rwkv.head_size`],
87
+ )
88
+ addIfDefined(
89
+ ggufSimplifiedMetadata,
90
+ 'rwkv_token_shift_count',
91
+ metadata[`${architecture}.rwkv.token_shift_count`],
92
+ )
93
+
94
+ return ggufSimplifiedMetadata
95
+ } catch (error) {
96
+ console.error('Failed to extract GGUF metadata:', error)
97
+ return null
98
+ }
99
+ }
100
+
101
+ type HFSibling = {
102
+ rfilename: string
103
+ size?: number
104
+ lfs?: { sha256?: string }
105
+ blobId?: string
106
+ }
107
+
108
+ type HFModel = {
109
+ id: string
110
+ author?: string
111
+ downloads?: number
112
+ likes?: number
113
+ tags?: string[]
114
+ pipeline_tag?: string
115
+ siblings?: HFSibling[]
116
+ config?: { model_type?: string }
117
+ cardData?: { model_type?: string }
118
+ }
119
+
120
+ const supportedLlmTasks = [
121
+ 'text-generation',
122
+ 'image-text-to-text',
123
+ 'text2text-generation',
124
+ 'conversational',
125
+ ]
126
+
127
+ type GeneratorType =
128
+ | 'GeneratorLLM'
129
+ | 'GeneratorVectorStore'
130
+ | 'GeneratorReranker'
131
+ | 'GeneratorGGMLTTS'
132
+ | 'GeneratorGGMLTTSVocoder'
133
+ | 'GeneratorOnnxLLM'
134
+ | 'GeneratorOnnxSTT'
135
+ | 'GeneratorTTS'
136
+ | 'GeneratorMlxLLM'
137
+
138
+ type ModelKind = 'gguf' | 'onnx' | 'mlx'
139
+
140
+ interface GeneratorConfig {
141
+ modelKind: ModelKind
142
+ filter: string
143
+ taskFilter?: string[]
144
+ filePattern?: RegExp
145
+ hasValidStructure?: (siblings: HFSibling[]) => boolean
146
+ }
147
+
148
+ // Helper to check valid ONNX structure
149
+ const hasValidOnnxStructure = (siblings: HFSibling[]): boolean => {
150
+ const hasConfigJson = siblings.some((file) => file.rfilename === 'config.json')
151
+ const hasOnnxModel = siblings.some(
152
+ (file) => file.rfilename.includes('onnx/') && file.rfilename.endsWith('.onnx'),
153
+ )
154
+ return hasConfigJson && hasOnnxModel
155
+ }
156
+
157
+ // Detect quantization types from ONNX files
158
+ const detectOnnxQuantizationTypes = (siblings: HFSibling[]): string[] => {
159
+ const onnxFiles = siblings.filter((file) => file.rfilename.endsWith('.onnx'))
160
+ const quantTypes = new Set<string>()
161
+
162
+ onnxFiles.forEach((file) => {
163
+ const filename = file.rfilename
164
+ if (!filename.endsWith('.onnx')) return
165
+ const postfix = /_(q8|q4|q4f16|fp16|int8|int4|uint8|bnb4|quantized)\.onnx$/.exec(filename)?.[1]
166
+ if (!postfix) {
167
+ quantTypes.add('auto')
168
+ quantTypes.add('none')
169
+ } else {
170
+ quantTypes.add(postfix === 'quantized' ? 'q8' : postfix)
171
+ }
172
+ })
173
+
174
+ return Array.from(quantTypes)
175
+ }
176
+
177
+ // Find speaker embedding files for TTS models
178
+ const findSpeakerEmbedFiles = (siblings: HFSibling[]): HFSibling[] =>
179
+ siblings.filter((file) => file.rfilename.startsWith('voices/') && file.rfilename.endsWith('.bin'))
180
+
181
+ const generatorConfigs: Record<GeneratorType, GeneratorConfig> = {
182
+ GeneratorLLM: {
183
+ modelKind: 'gguf',
184
+ filter: 'gguf',
185
+ taskFilter: supportedLlmTasks,
186
+ filePattern: /\.gguf$/,
187
+ },
188
+ GeneratorVectorStore: {
189
+ modelKind: 'gguf',
190
+ filter: 'gguf',
191
+ filePattern: /\.gguf$/,
192
+ },
193
+ GeneratorReranker: {
194
+ modelKind: 'gguf',
195
+ filter: 'gguf,reranker',
196
+ filePattern: /\.gguf$/,
197
+ },
198
+ GeneratorGGMLTTS: {
199
+ modelKind: 'gguf',
200
+ filter: 'gguf,text-to-speech',
201
+ filePattern: /\.gguf$/,
202
+ },
203
+ GeneratorGGMLTTSVocoder: {
204
+ modelKind: 'gguf',
205
+ filter: 'gguf,feature-extraction',
206
+ filePattern: /\.gguf$/,
207
+ },
208
+ GeneratorOnnxLLM: {
209
+ modelKind: 'onnx',
210
+ filter: 'onnx',
211
+ taskFilter: supportedLlmTasks,
212
+ hasValidStructure: hasValidOnnxStructure,
213
+ },
214
+ GeneratorOnnxSTT: {
215
+ modelKind: 'onnx',
216
+ filter: 'onnx,automatic-speech-recognition',
217
+ hasValidStructure: hasValidOnnxStructure,
218
+ },
219
+ GeneratorTTS: {
220
+ modelKind: 'onnx',
221
+ filter: 'onnx,text-to-speech',
222
+ hasValidStructure: hasValidOnnxStructure,
223
+ },
224
+ GeneratorMlxLLM: {
225
+ modelKind: 'mlx',
226
+ filter: 'mlx',
227
+ taskFilter: supportedLlmTasks,
228
+ },
229
+ }
230
+
231
+ const searchHFModels = async (filter: string, search?: string, limit = 50): Promise<HFModel[]> => {
232
+ const params = new URLSearchParams({
233
+ limit: String(limit),
234
+ full: 'true',
235
+ config: 'true',
236
+ sort: 'likes',
237
+ direction: '-1',
238
+ })
239
+ if (filter) params.set('filter', filter)
240
+ if (search) params.set('search', search)
241
+
242
+ const headers: Record<string, string> = {}
243
+ if (HF_TOKEN) {
244
+ headers['Authorization'] = `Bearer ${HF_TOKEN}`
245
+ }
246
+
247
+ const response = await fetch(`${HF_API_URL}/models?${params.toString()}`, { headers })
248
+ if (!response.ok) {
249
+ throw new Error(`Hugging Face API error: ${response.status} ${response.statusText}`)
250
+ }
251
+ return response.json()
252
+ }
253
+
254
+ const fetchHFModelDetails = async (modelId: string): Promise<HFModel> => {
255
+ const params = new URLSearchParams({ blobs: 'true' })
256
+
257
+ const headers: Record<string, string> = {}
258
+ if (HF_TOKEN) {
259
+ headers['Authorization'] = `Bearer ${HF_TOKEN}`
260
+ }
261
+
262
+ const response = await fetch(`${HF_API_URL}/models/${modelId}?${params.toString()}`, { headers })
263
+ if (!response.ok) {
264
+ throw new Error(`Hugging Face API error: ${response.status} ${response.statusText}`)
265
+ }
266
+ return response.json()
267
+ }
268
+
269
+ // Example: Mixtral-8x22B-v0.1.IQ3_XS-00001-of-00005.gguf
270
+ const ggufSplitPattern = /-(\d{5})-of-(\d{5})\.gguf$/
271
+
272
+ export function register(server: McpServer) {
273
+ server.tool(
274
+ 'huggingface_search',
275
+ {
276
+ generatorType: z
277
+ .enum([
278
+ 'GeneratorLLM',
279
+ 'GeneratorVectorStore',
280
+ 'GeneratorReranker',
281
+ 'GeneratorGGMLTTS',
282
+ 'GeneratorGGMLTTSVocoder',
283
+ 'GeneratorOnnxLLM',
284
+ 'GeneratorOnnxSTT',
285
+ 'GeneratorTTS',
286
+ 'GeneratorMlxLLM',
287
+ ])
288
+ .describe('Generator type to search models for')
289
+ .default('GeneratorLLM'),
290
+ query: z.string().describe('Search keywords for models on Hugging Face').optional(),
291
+ limit: z.number().min(1).max(100).optional().default(20),
292
+ includeFiles: z
293
+ .boolean()
294
+ .optional()
295
+ .default(false)
296
+ .describe('Include list of model files (requires additional API calls)'),
297
+ },
298
+ async ({ generatorType, query, limit, includeFiles }) => {
299
+ try {
300
+ const config = generatorConfigs[generatorType]
301
+ const models = await searchHFModels(config.filter, query, limit)
302
+
303
+ // Filter models based on generator configuration
304
+ const filteredModels = models.filter((model) => {
305
+ const modelTags = model.tags || []
306
+
307
+ // Check task filter if configured
308
+ if (config.taskFilter && !config.taskFilter.some((t) => modelTags.includes(t))) {
309
+ return false
310
+ }
311
+
312
+ // Check structure validation for ONNX models
313
+ if (config.hasValidStructure && model.siblings) {
314
+ if (!config.hasValidStructure(model.siblings)) {
315
+ return false
316
+ }
317
+ }
318
+
319
+ return true
320
+ })
321
+
322
+ // Build result models
323
+ let results: Array<{
324
+ id: string
325
+ author?: string
326
+ downloads?: number
327
+ likes?: number
328
+ pipeline_tag?: string
329
+ model_type?: string
330
+ model_kind: ModelKind
331
+ files?: Array<{ filename: string; size?: number }>
332
+ quantization_types?: string[]
333
+ speaker_embed_files?: Array<{ filename: string; size?: number }>
334
+ }> = filteredModels.map((model) => ({
335
+ id: model.id,
336
+ author: model.author,
337
+ downloads: model.downloads,
338
+ likes: model.likes,
339
+ pipeline_tag: model.pipeline_tag,
340
+ model_type: model.config?.model_type || model.cardData?.model_type,
341
+ model_kind: config.modelKind,
342
+ }))
343
+
344
+ if (includeFiles) {
345
+ results = await Promise.all(
346
+ results.map(async (model) => {
347
+ try {
348
+ const details = await fetchHFModelDetails(model.id)
349
+ const siblings = details.siblings || []
350
+
351
+ if (config.modelKind === 'gguf') {
352
+ const ggufFiles = siblings
353
+ .filter((file) => config.filePattern?.test(file.rfilename))
354
+ .map((file) => ({
355
+ filename: file.rfilename,
356
+ size: file.size,
357
+ }))
358
+ return { ...model, files: ggufFiles }
359
+ } else {
360
+ // ONNX models
361
+ const quantTypes = detectOnnxQuantizationTypes(siblings)
362
+ const speakerFiles =
363
+ generatorType === 'GeneratorTTS' ? findSpeakerEmbedFiles(siblings) : []
364
+
365
+ return {
366
+ ...model,
367
+ quantization_types: quantTypes,
368
+ ...(speakerFiles.length > 0 && {
369
+ speaker_embed_files: speakerFiles.map((f) => ({
370
+ filename: f.rfilename,
371
+ size: f.size,
372
+ })),
373
+ }),
374
+ }
375
+ }
376
+ } catch {
377
+ return model
378
+ }
379
+ }),
380
+ )
381
+ }
382
+
383
+ return {
384
+ content: [
385
+ {
386
+ type: 'text',
387
+ text: TOON.encode({
388
+ count: results.length,
389
+ generatorType,
390
+ modelKind: config.modelKind,
391
+ models: results,
392
+ hf_token_configured: !!HF_TOKEN,
393
+ }),
394
+ },
395
+ ],
396
+ }
397
+ } catch (err: any) {
398
+ return {
399
+ content: [{ type: 'text', text: `Failed to search models: ${err.message}` }],
400
+ }
401
+ }
402
+ },
403
+ )
404
+
405
+ server.tool(
406
+ 'huggingface_select',
407
+ {
408
+ generatorType: z
409
+ .enum([
410
+ 'GeneratorLLM',
411
+ 'GeneratorVectorStore',
412
+ 'GeneratorReranker',
413
+ 'GeneratorGGMLTTS',
414
+ 'GeneratorGGMLTTSVocoder',
415
+ 'GeneratorOnnxLLM',
416
+ 'GeneratorOnnxSTT',
417
+ 'GeneratorTTS',
418
+ 'GeneratorMlxLLM',
419
+ ])
420
+ .describe('Generator type for model selection')
421
+ .default('GeneratorLLM'),
422
+ // eslint-disable-next-line camelcase
423
+ model_id: z
424
+ .string()
425
+ .describe('Hugging Face model ID (e.g., "unsloth/Llama-3.2-1B-Instruct-GGUF")'),
426
+ filename: z
427
+ .string()
428
+ .describe('Model filename to select (required for GGUF models)')
429
+ .optional(),
430
+ quantize_type: z
431
+ .string()
432
+ .describe('Quantization type for ONNX models (e.g., "q8", "fp16", "auto")')
433
+ .optional()
434
+ .default('auto'),
435
+ speaker_embed_file: z.string().describe('Speaker embedding file for TTS models').optional(),
436
+ },
437
+ // eslint-disable-next-line camelcase
438
+ async ({
439
+ generatorType,
440
+ model_id: modelId,
441
+ filename,
442
+ quantize_type: quantizeType,
443
+ speaker_embed_file: speakerEmbedFile,
444
+ }) => {
445
+ try {
446
+ const config = generatorConfigs[generatorType]
447
+ const details = await fetchHFModelDetails(modelId)
448
+ const siblings = details.siblings || []
449
+
450
+ // Handle ONNX models
451
+ // ONNX generators expect: model (HF model ID), modelType, quantizeType
452
+ if (config.modelKind === 'onnx') {
453
+ const quantTypes = detectOnnxQuantizationTypes(siblings)
454
+ const speakerFiles =
455
+ generatorType === 'GeneratorTTS' ? findSpeakerEmbedFiles(siblings) : []
456
+ const selectedSpeakerFile = speakerEmbedFile
457
+ ? siblings.find((f) => f.rfilename === speakerEmbedFile)
458
+ : undefined
459
+
460
+ const selectedQuantType = quantTypes.includes(quantizeType || 'auto')
461
+ ? quantizeType
462
+ : 'auto'
463
+ const modelType = details.config?.model_type || details.cardData?.model_type
464
+
465
+ // Result format matches ONNX generator property names (camelCase)
466
+ const result = {
467
+ // Primary model fields for generator
468
+ model: modelId,
469
+ modelType,
470
+ quantizeType: selectedQuantType,
471
+ // Speaker embedding for TTS generators
472
+ ...(selectedSpeakerFile && {
473
+ speakerEmbedUrl: `https://huggingface.co/${modelId}/resolve/main/${selectedSpeakerFile.rfilename}?download=true`,
474
+ speakerEmbedHash: selectedSpeakerFile.lfs?.sha256,
475
+ speakerEmbedHashType: 'sha256',
476
+ }),
477
+ // Additional info
478
+ availableQuantizeTypes: quantTypes,
479
+ ...(speakerFiles.length > 0 && {
480
+ availableSpeakerEmbedFiles: speakerFiles.map((f) => f.rfilename),
481
+ }),
482
+ _hfRepoInfo: {
483
+ repo: modelId,
484
+ model: {
485
+ id: details.id,
486
+ downloads: details.downloads,
487
+ likes: details.likes,
488
+ author: details.author,
489
+ },
490
+ generatorType,
491
+ modelType,
492
+ quantizeType: selectedQuantType,
493
+ ...(selectedSpeakerFile && {
494
+ speakerEmbedFile: selectedSpeakerFile.rfilename,
495
+ }),
496
+ },
497
+ }
498
+
499
+ // Return JSON for huggingface_select to allow direct parsing by consumers
500
+ return {
501
+ content: [
502
+ {
503
+ type: 'text',
504
+ text: `${JSON5.stringify(result, null, 2)} // Please use _hfRepoInfo undocumented field with @ts-ignore`,
505
+ },
506
+ ],
507
+ }
508
+ }
509
+
510
+ // Handle MLX models (repo-level, like ONNX)
511
+ if (config.modelKind === 'mlx') {
512
+ // Fetch config.json for model architecture metadata
513
+ const headers: Record<string, string> = {}
514
+ if (HF_TOKEN) headers['Authorization'] = `Bearer ${HF_TOKEN}`
515
+ let mlxModelConfig: Record<string, any> | null = null
516
+ try {
517
+ const configRes = await fetch(
518
+ `https://huggingface.co/${modelId}/resolve/main/config.json`,
519
+ { headers },
520
+ )
521
+ if (configRes.ok) mlxModelConfig = await configRes.json()
522
+ } catch {
523
+ // Non-critical
524
+ }
525
+
526
+ const modelType =
527
+ mlxModelConfig?.model_type || details.config?.model_type || details.cardData?.model_type
528
+
529
+ // Build MLX model metadata from config.json (for hardware guardrails)
530
+ const cfg = mlxModelConfig || ({} as Record<string, any>)
531
+ const textCfg = cfg.text_config || cfg
532
+ const numHeads = textCfg.num_attention_heads || textCfg.n_heads || 0
533
+ const hiddenSize = textCfg.hidden_size || textCfg.dim || 0
534
+ const kvLoraRank = textCfg.kv_lora_rank || 0
535
+ const quant = cfg.quantization || cfg.quantization_config || null
536
+
537
+ // Sum safetensors/npz file sizes for model weight bytes
538
+ const modelBytes = siblings
539
+ .filter((f) => /\.(safetensors|npz)$/.test(f.rfilename))
540
+ .reduce((sum, f) => sum + (f.size ?? 0), 0)
541
+
542
+ // Build _mlxDownloadFiles list (safetensors, json, jinja, tokenizer.model)
543
+ const mlxDownloadFiles = siblings
544
+ .filter(
545
+ (f) =>
546
+ f.rfilename.endsWith('.safetensors') ||
547
+ f.rfilename.endsWith('.json') ||
548
+ f.rfilename.endsWith('.jinja') ||
549
+ f.rfilename === 'tokenizer.model',
550
+ )
551
+ .map((f) => ({
552
+ url: `https://huggingface.co/${modelId}/resolve/main/${f.rfilename}?download=true`,
553
+ filename: `${modelId.replace('/', '-')}/${f.rfilename}`,
554
+ // eslint-disable-next-line no-nested-ternary
555
+ hash_type: f.lfs ? 'sha256' : f.blobId ? 'sha1' : undefined,
556
+ sha256: f.lfs?.sha256,
557
+ sha1: f.lfs ? undefined : f.blobId,
558
+ }))
559
+
560
+ const result = {
561
+ modelId,
562
+ modelType,
563
+ _mlxDownloadFiles: mlxDownloadFiles,
564
+ _hfRepoInfo: {
565
+ repo: modelId,
566
+ model: {
567
+ id: details.id,
568
+ downloads: details.downloads,
569
+ likes: details.likes,
570
+ author: details.author,
571
+ },
572
+ generatorType,
573
+ modelType,
574
+ ...(modelBytes > 0 && { size: modelBytes }),
575
+ ...(mlxModelConfig && {
576
+ mlxModelMetadata: {
577
+ hiddenSize,
578
+ numLayers: textCfg.num_hidden_layers || textCfg.n_layers || 0,
579
+ numHeads,
580
+ numKvHeads: textCfg.num_key_value_heads ?? numHeads,
581
+ headDim:
582
+ textCfg.head_dim ||
583
+ textCfg.v_head_dim ||
584
+ (numHeads > 0 && hiddenSize > 0 ? hiddenSize / numHeads : 0),
585
+ maxCtx: textCfg.max_position_embeddings || 0,
586
+ kvLoraRank,
587
+ qkRopeHeadDim: textCfg.qk_rope_head_dim || 0,
588
+ isMLA: kvLoraRank > 0,
589
+ quantBits: quant?.bits || null,
590
+ quantGroupSize: quant?.group_size || null,
591
+ },
592
+ }),
593
+ },
594
+ }
595
+
596
+ return {
597
+ content: [
598
+ {
599
+ type: 'text',
600
+ text: `${JSON5.stringify(result, null, 2)} // IMPORTANT: Use _mlxDownloadFiles / _hfRepoInfo undocumented fields with @ts-ignore`,
601
+ },
602
+ ],
603
+ }
604
+ }
605
+
606
+ // Handle GGUF models
607
+ if (!filename) {
608
+ // List available GGUF files
609
+ const ggufFiles = siblings
610
+ .filter((file) => config.filePattern?.test(file.rfilename))
611
+ .map((file) => ({
612
+ filename: file.rfilename,
613
+ size: file.size,
614
+ }))
615
+
616
+ // Return JSON for huggingface_select to allow direct parsing by consumers
617
+ return {
618
+ content: [
619
+ {
620
+ type: 'text',
621
+ text: JSON.stringify(
622
+ {
623
+ error: 'filename is required for GGUF models',
624
+ available_files: ggufFiles,
625
+ },
626
+ null,
627
+ 2,
628
+ ),
629
+ },
630
+ ],
631
+ }
632
+ }
633
+
634
+ // Find the selected file
635
+ const selectedFile = siblings.find((f) => f.rfilename === filename)
636
+ if (!selectedFile) {
637
+ return {
638
+ content: [{ type: 'text', text: `File "${filename}" not found in model ${modelId}` }],
639
+ }
640
+ }
641
+
642
+ // Check if it's a split file
643
+ const matched = filename.match(ggufSplitPattern)
644
+ const isSplit = !!matched
645
+
646
+ // Find mmproj file if available (only for LLM generators)
647
+ const mmprojFile =
648
+ generatorType === 'GeneratorLLM'
649
+ ? siblings.find((f) => /^mmproj-/i.test(f.rfilename))
650
+ : undefined
651
+
652
+ // Extract GGUF metadata (for split files, metadata is in the first split)
653
+ const metadataFilename = isSplit
654
+ ? filename.replace(ggufSplitPattern, '-00001-of-$2.gguf')
655
+ : filename
656
+ const ggufUrl = `https://huggingface.co/${modelId}/resolve/main/${metadataFilename}`
657
+ const ggufSimplifiedMetadata = await extractGGUFMetadata(ggufUrl)
658
+
659
+ if (isSplit) {
660
+ const [, , splitTotal] = matched!
661
+ const splitFiles = Array.from({ length: Number(splitTotal) }, (_, i) => {
662
+ const split = String(i + 1).padStart(5, '0')
663
+ const splitRFilename = filename.replace(
664
+ ggufSplitPattern,
665
+ `-${split}-of-${splitTotal}.gguf`,
666
+ )
667
+ const sibling = siblings.find((sb) => sb.rfilename === splitRFilename)
668
+ return {
669
+ rfilename: splitRFilename,
670
+ size: sibling?.size,
671
+ lfs: sibling?.lfs,
672
+ }
673
+ })
674
+
675
+ const first = splitFiles[0]
676
+ const rest = splitFiles.slice(1)
677
+
678
+ const result = {
679
+ url: `https://huggingface.co/${modelId}/resolve/main/${first.rfilename}?download=true`,
680
+ hash: first.lfs?.sha256,
681
+ hash_type: 'sha256',
682
+ _ggufSplitFiles: rest.map((split) => ({
683
+ url: `https://huggingface.co/${modelId}/resolve/main/${split.rfilename}?download=true`,
684
+ hash: split.lfs?.sha256,
685
+ hash_type: 'sha256',
686
+ })),
687
+ ...(mmprojFile && {
688
+ mmproj_url: `https://huggingface.co/${modelId}/resolve/main/${mmprojFile.rfilename}?download=true`,
689
+ mmproj_hash: mmprojFile.lfs?.sha256,
690
+ mmproj_hash_type: 'sha256',
691
+ }),
692
+ _hfRepoInfo: {
693
+ repo: modelId,
694
+ model: {
695
+ id: details.id,
696
+ downloads: details.downloads,
697
+ likes: details.likes,
698
+ author: details.author,
699
+ },
700
+ generatorType,
701
+ isSplit: true,
702
+ files: splitFiles.map((f) => f.rfilename),
703
+ sizes: splitFiles.map((f) => f.size),
704
+ size: splitFiles.reduce((acc, f) => acc + (f.size ?? 0), 0),
705
+ ...(mmprojFile && {
706
+ mmprojFile: mmprojFile.rfilename,
707
+ mmprojSize: mmprojFile.size,
708
+ }),
709
+ ...(ggufSimplifiedMetadata && { ggufSimplifiedMetadata }),
710
+ },
711
+ }
712
+
713
+ // Return JSON for huggingface_select to allow direct parsing by consumers
714
+ return {
715
+ content: [{ type: 'text', text: JSON5.stringify(result, null, 2) }],
716
+ }
717
+ } else {
718
+ const result = {
719
+ url: `https://huggingface.co/${modelId}/resolve/main/${filename}?download=true`,
720
+ hash: selectedFile.lfs?.sha256,
721
+ hash_type: 'sha256',
722
+ ...(mmprojFile && {
723
+ mmproj_url: `https://huggingface.co/${modelId}/resolve/main/${mmprojFile.rfilename}?download=true`,
724
+ mmproj_hash: mmprojFile.lfs?.sha256,
725
+ mmproj_hash_type: 'sha256',
726
+ }),
727
+ _hfRepoInfo: {
728
+ repo: modelId,
729
+ model: {
730
+ id: details.id,
731
+ downloads: details.downloads,
732
+ likes: details.likes,
733
+ author: details.author,
734
+ },
735
+ generatorType,
736
+ file: filename,
737
+ size: selectedFile.size,
738
+ ...(mmprojFile && {
739
+ mmprojFile: mmprojFile.rfilename,
740
+ mmprojSize: mmprojFile.size,
741
+ }),
742
+ ...(ggufSimplifiedMetadata && { ggufSimplifiedMetadata }),
743
+ },
744
+ }
745
+
746
+ // Return JSON for huggingface_select to allow direct parsing by consumers
747
+ return {
748
+ content: [
749
+ {
750
+ type: 'text',
751
+ text: `${JSON5.stringify(result, null, 2)} // IMPORTANT: Use _hfRepoInfo undocumented field with @ts-ignore`,
752
+ },
753
+ ],
754
+ }
755
+ }
756
+ } catch (err: any) {
757
+ return {
758
+ content: [{ type: 'text', text: `Failed to select model: ${err.message}` }],
759
+ }
760
+ }
761
+ },
762
+ )
763
+ }