@fugood/bricks-project 2.24.0-beta.2 → 2.24.0-beta.21
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.
- package/compile/action-name-map.ts +14 -0
- package/compile/index.ts +377 -129
- package/package.json +8 -3
- package/skills/bricks-project/rules/architecture-patterns.md +7 -0
- package/skills/bricks-project/rules/automations.md +74 -28
- package/skills/bricks-project/rules/buttress.md +9 -6
- package/tools/deploy.ts +39 -10
- package/tools/mcp-server.ts +10 -877
- package/tools/mcp-tools/compile.ts +91 -0
- package/tools/mcp-tools/huggingface.ts +762 -0
- package/tools/mcp-tools/icons.ts +60 -0
- package/tools/mcp-tools/lottie.ts +102 -0
- package/tools/mcp-tools/media.ts +110 -0
- package/tools/postinstall.ts +121 -33
- package/tools/preview-main.mjs +12 -8
- package/tools/pull.ts +37 -19
- package/tsconfig.json +16 -0
- package/types/animation.ts +4 -0
- package/types/automation.ts +3 -0
- package/types/brick-base.ts +1 -1
- package/types/bricks/Camera.ts +34 -7
- package/types/bricks/Chart.ts +1 -1
- package/types/bricks/GenerativeMedia.ts +6 -6
- package/types/bricks/Icon.ts +3 -3
- package/types/bricks/Image.ts +4 -4
- package/types/bricks/Items.ts +7 -7
- package/types/bricks/Lottie.ts +4 -4
- package/types/bricks/Maps.ts +4 -4
- package/types/bricks/QrCode.ts +4 -4
- package/types/bricks/Rect.ts +4 -4
- package/types/bricks/RichText.ts +3 -3
- package/types/bricks/Rive.ts +1 -1
- package/types/bricks/Slideshow.ts +4 -4
- package/types/bricks/Svg.ts +3 -3
- package/types/bricks/Text.ts +4 -4
- package/types/bricks/TextInput.ts +11 -7
- package/types/bricks/Video.ts +4 -4
- package/types/bricks/VideoStreaming.ts +3 -3
- package/types/bricks/WebRtcStream.ts +1 -1
- package/types/bricks/WebView.ts +4 -4
- package/types/canvas.ts +4 -2
- package/types/common.ts +9 -4
- package/types/data-calc-command.ts +2 -0
- package/types/data-calc.ts +1 -0
- package/types/data.ts +2 -0
- package/types/generators/AlarmClock.ts +5 -5
- package/types/generators/Assistant.ts +57 -12
- package/types/generators/BleCentral.ts +12 -4
- package/types/generators/BlePeripheral.ts +5 -5
- package/types/generators/CanvasMap.ts +4 -4
- package/types/generators/CastlesPay.ts +3 -3
- package/types/generators/DataBank.ts +31 -4
- package/types/generators/File.ts +63 -14
- package/types/generators/GraphQl.ts +3 -3
- package/types/generators/Http.ts +27 -8
- package/types/generators/HttpServer.ts +9 -9
- package/types/generators/Information.ts +2 -2
- package/types/generators/Intent.ts +8 -2
- package/types/generators/Iterator.ts +6 -6
- package/types/generators/Keyboard.ts +18 -8
- package/types/generators/LlmAnthropicCompat.ts +12 -6
- package/types/generators/LlmAppleBuiltin.ts +6 -6
- package/types/generators/LlmGgml.ts +75 -25
- package/types/generators/LlmMlx.ts +210 -0
- package/types/generators/LlmOnnx.ts +18 -9
- package/types/generators/LlmOpenAiCompat.ts +22 -6
- package/types/generators/LlmQualcommAiEngine.ts +32 -8
- package/types/generators/Mcp.ts +332 -17
- package/types/generators/McpServer.ts +38 -11
- package/types/generators/MediaFlow.ts +26 -8
- package/types/generators/MqttBroker.ts +10 -4
- package/types/generators/MqttClient.ts +11 -5
- package/types/generators/Question.ts +6 -6
- package/types/generators/RealtimeTranscription.ts +70 -11
- package/types/generators/RerankerGgml.ts +23 -9
- package/types/generators/SerialPort.ts +6 -6
- package/types/generators/SoundPlayer.ts +2 -2
- package/types/generators/SoundRecorder.ts +5 -5
- package/types/generators/SpeechToTextGgml.ts +34 -14
- package/types/generators/SpeechToTextOnnx.ts +8 -8
- package/types/generators/SpeechToTextPlatform.ts +4 -4
- package/types/generators/SqLite.ts +10 -6
- package/types/generators/Step.ts +3 -3
- package/types/generators/SttAppleBuiltin.ts +6 -6
- package/types/generators/Tcp.ts +5 -5
- package/types/generators/TcpServer.ts +7 -7
- package/types/generators/TextToSpeechApple.ts +1 -1
- package/types/generators/TextToSpeechAppleBuiltin.ts +5 -5
- package/types/generators/TextToSpeechGgml.ts +8 -8
- package/types/generators/TextToSpeechOnnx.ts +9 -9
- package/types/generators/TextToSpeechOpenAiLike.ts +5 -5
- package/types/generators/ThermalPrinter.ts +6 -6
- package/types/generators/Tick.ts +3 -3
- package/types/generators/Udp.ts +9 -4
- package/types/generators/VadGgml.ts +39 -10
- package/types/generators/VadOnnx.ts +31 -8
- package/types/generators/VadTraditional.ts +15 -9
- package/types/generators/VectorStore.ts +26 -9
- package/types/generators/Watchdog.ts +11 -6
- package/types/generators/WebCrawler.ts +5 -5
- package/types/generators/WebRtc.ts +17 -11
- package/types/generators/WebSocket.ts +5 -5
- package/types/generators/index.ts +1 -0
- package/types/subspace.ts +1 -0
- package/types/system.ts +1 -1
- package/utils/calc.ts +12 -8
- package/utils/event-props.ts +104 -87
- package/utils/id.ts +4 -0
- package/api/index.ts +0 -1
- package/api/instance.ts +0 -213
|
@@ -0,0 +1,762 @@
|
|
|
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
|
+
hash_type: f.lfs ? 'sha256' : f.blobId ? 'sha1' : undefined,
|
|
555
|
+
sha256: f.lfs?.sha256,
|
|
556
|
+
sha1: f.lfs ? undefined : f.blobId,
|
|
557
|
+
}))
|
|
558
|
+
|
|
559
|
+
const result = {
|
|
560
|
+
modelId,
|
|
561
|
+
modelType,
|
|
562
|
+
_mlxDownloadFiles: mlxDownloadFiles,
|
|
563
|
+
_hfRepoInfo: {
|
|
564
|
+
repo: modelId,
|
|
565
|
+
model: {
|
|
566
|
+
id: details.id,
|
|
567
|
+
downloads: details.downloads,
|
|
568
|
+
likes: details.likes,
|
|
569
|
+
author: details.author,
|
|
570
|
+
},
|
|
571
|
+
generatorType,
|
|
572
|
+
modelType,
|
|
573
|
+
...(modelBytes > 0 && { size: modelBytes }),
|
|
574
|
+
...(mlxModelConfig && {
|
|
575
|
+
mlxModelMetadata: {
|
|
576
|
+
hiddenSize,
|
|
577
|
+
numLayers: textCfg.num_hidden_layers || textCfg.n_layers || 0,
|
|
578
|
+
numHeads,
|
|
579
|
+
numKvHeads: textCfg.num_key_value_heads ?? numHeads,
|
|
580
|
+
headDim:
|
|
581
|
+
textCfg.head_dim ||
|
|
582
|
+
textCfg.v_head_dim ||
|
|
583
|
+
(numHeads > 0 && hiddenSize > 0 ? hiddenSize / numHeads : 0),
|
|
584
|
+
maxCtx: textCfg.max_position_embeddings || 0,
|
|
585
|
+
kvLoraRank,
|
|
586
|
+
qkRopeHeadDim: textCfg.qk_rope_head_dim || 0,
|
|
587
|
+
isMLA: kvLoraRank > 0,
|
|
588
|
+
quantBits: quant?.bits || null,
|
|
589
|
+
quantGroupSize: quant?.group_size || null,
|
|
590
|
+
},
|
|
591
|
+
}),
|
|
592
|
+
},
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
content: [
|
|
597
|
+
{
|
|
598
|
+
type: 'text',
|
|
599
|
+
text: `${JSON5.stringify(result, null, 2)} // IMPORTANT: Use _mlxDownloadFiles / _hfRepoInfo undocumented fields with @ts-ignore`,
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Handle GGUF models
|
|
606
|
+
if (!filename) {
|
|
607
|
+
// List available GGUF files
|
|
608
|
+
const ggufFiles = siblings
|
|
609
|
+
.filter((file) => config.filePattern?.test(file.rfilename))
|
|
610
|
+
.map((file) => ({
|
|
611
|
+
filename: file.rfilename,
|
|
612
|
+
size: file.size,
|
|
613
|
+
}))
|
|
614
|
+
|
|
615
|
+
// Return JSON for huggingface_select to allow direct parsing by consumers
|
|
616
|
+
return {
|
|
617
|
+
content: [
|
|
618
|
+
{
|
|
619
|
+
type: 'text',
|
|
620
|
+
text: JSON.stringify(
|
|
621
|
+
{
|
|
622
|
+
error: 'filename is required for GGUF models',
|
|
623
|
+
available_files: ggufFiles,
|
|
624
|
+
},
|
|
625
|
+
null,
|
|
626
|
+
2,
|
|
627
|
+
),
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Find the selected file
|
|
634
|
+
const selectedFile = siblings.find((f) => f.rfilename === filename)
|
|
635
|
+
if (!selectedFile) {
|
|
636
|
+
return {
|
|
637
|
+
content: [{ type: 'text', text: `File "${filename}" not found in model ${modelId}` }],
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Check if it's a split file
|
|
642
|
+
const matched = filename.match(ggufSplitPattern)
|
|
643
|
+
const isSplit = !!matched
|
|
644
|
+
|
|
645
|
+
// Find mmproj file if available (only for LLM generators)
|
|
646
|
+
const mmprojFile =
|
|
647
|
+
generatorType === 'GeneratorLLM'
|
|
648
|
+
? siblings.find((f) => /^mmproj-/i.test(f.rfilename))
|
|
649
|
+
: undefined
|
|
650
|
+
|
|
651
|
+
// Extract GGUF metadata (for split files, metadata is in the first split)
|
|
652
|
+
const metadataFilename = isSplit
|
|
653
|
+
? filename.replace(ggufSplitPattern, '-00001-of-$2.gguf')
|
|
654
|
+
: filename
|
|
655
|
+
const ggufUrl = `https://huggingface.co/${modelId}/resolve/main/${metadataFilename}`
|
|
656
|
+
const ggufSimplifiedMetadata = await extractGGUFMetadata(ggufUrl)
|
|
657
|
+
|
|
658
|
+
if (isSplit) {
|
|
659
|
+
const [, , splitTotal] = matched!
|
|
660
|
+
const splitFiles = Array.from({ length: Number(splitTotal) }, (_, i) => {
|
|
661
|
+
const split = String(i + 1).padStart(5, '0')
|
|
662
|
+
const splitRFilename = filename.replace(
|
|
663
|
+
ggufSplitPattern,
|
|
664
|
+
`-${split}-of-${splitTotal}.gguf`,
|
|
665
|
+
)
|
|
666
|
+
const sibling = siblings.find((sb) => sb.rfilename === splitRFilename)
|
|
667
|
+
return {
|
|
668
|
+
rfilename: splitRFilename,
|
|
669
|
+
size: sibling?.size,
|
|
670
|
+
lfs: sibling?.lfs,
|
|
671
|
+
}
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
const first = splitFiles[0]
|
|
675
|
+
const rest = splitFiles.slice(1)
|
|
676
|
+
|
|
677
|
+
const result = {
|
|
678
|
+
url: `https://huggingface.co/${modelId}/resolve/main/${first.rfilename}?download=true`,
|
|
679
|
+
hash: first.lfs?.sha256,
|
|
680
|
+
hash_type: 'sha256',
|
|
681
|
+
_ggufSplitFiles: rest.map((split) => ({
|
|
682
|
+
url: `https://huggingface.co/${modelId}/resolve/main/${split.rfilename}?download=true`,
|
|
683
|
+
hash: split.lfs?.sha256,
|
|
684
|
+
hash_type: 'sha256',
|
|
685
|
+
})),
|
|
686
|
+
...(mmprojFile && {
|
|
687
|
+
mmproj_url: `https://huggingface.co/${modelId}/resolve/main/${mmprojFile.rfilename}?download=true`,
|
|
688
|
+
mmproj_hash: mmprojFile.lfs?.sha256,
|
|
689
|
+
mmproj_hash_type: 'sha256',
|
|
690
|
+
}),
|
|
691
|
+
_hfRepoInfo: {
|
|
692
|
+
repo: modelId,
|
|
693
|
+
model: {
|
|
694
|
+
id: details.id,
|
|
695
|
+
downloads: details.downloads,
|
|
696
|
+
likes: details.likes,
|
|
697
|
+
author: details.author,
|
|
698
|
+
},
|
|
699
|
+
generatorType,
|
|
700
|
+
isSplit: true,
|
|
701
|
+
files: splitFiles.map((f) => f.rfilename),
|
|
702
|
+
sizes: splitFiles.map((f) => f.size),
|
|
703
|
+
size: splitFiles.reduce((acc, f) => acc + (f.size ?? 0), 0),
|
|
704
|
+
...(mmprojFile && {
|
|
705
|
+
mmprojFile: mmprojFile.rfilename,
|
|
706
|
+
mmprojSize: mmprojFile.size,
|
|
707
|
+
}),
|
|
708
|
+
...(ggufSimplifiedMetadata && { ggufSimplifiedMetadata }),
|
|
709
|
+
},
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Return JSON for huggingface_select to allow direct parsing by consumers
|
|
713
|
+
return {
|
|
714
|
+
content: [{ type: 'text', text: JSON5.stringify(result, null, 2) }],
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
const result = {
|
|
718
|
+
url: `https://huggingface.co/${modelId}/resolve/main/${filename}?download=true`,
|
|
719
|
+
hash: selectedFile.lfs?.sha256,
|
|
720
|
+
hash_type: 'sha256',
|
|
721
|
+
...(mmprojFile && {
|
|
722
|
+
mmproj_url: `https://huggingface.co/${modelId}/resolve/main/${mmprojFile.rfilename}?download=true`,
|
|
723
|
+
mmproj_hash: mmprojFile.lfs?.sha256,
|
|
724
|
+
mmproj_hash_type: 'sha256',
|
|
725
|
+
}),
|
|
726
|
+
_hfRepoInfo: {
|
|
727
|
+
repo: modelId,
|
|
728
|
+
model: {
|
|
729
|
+
id: details.id,
|
|
730
|
+
downloads: details.downloads,
|
|
731
|
+
likes: details.likes,
|
|
732
|
+
author: details.author,
|
|
733
|
+
},
|
|
734
|
+
generatorType,
|
|
735
|
+
file: filename,
|
|
736
|
+
size: selectedFile.size,
|
|
737
|
+
...(mmprojFile && {
|
|
738
|
+
mmprojFile: mmprojFile.rfilename,
|
|
739
|
+
mmprojSize: mmprojFile.size,
|
|
740
|
+
}),
|
|
741
|
+
...(ggufSimplifiedMetadata && { ggufSimplifiedMetadata }),
|
|
742
|
+
},
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Return JSON for huggingface_select to allow direct parsing by consumers
|
|
746
|
+
return {
|
|
747
|
+
content: [
|
|
748
|
+
{
|
|
749
|
+
type: 'text',
|
|
750
|
+
text: `${JSON5.stringify(result, null, 2)} // IMPORTANT: Use _hfRepoInfo undocumented field with @ts-ignore`,
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
} catch (err: any) {
|
|
756
|
+
return {
|
|
757
|
+
content: [{ type: 'text', text: `Failed to select model: ${err.message}` }],
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
)
|
|
762
|
+
}
|