offgrid-ai 0.18.1 → 0.18.2
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/package.json +1 -1
- package/src/download.mjs +18 -2
- package/src/huggingface.mjs +24 -2
package/package.json
CHANGED
package/src/download.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Model download flow — HuggingFace downloads with quant picker and RAM fit.
|
|
2
2
|
// Used by onboarding (no models found) and the model picker (↓ Download a model).
|
|
3
3
|
|
|
4
|
-
import { hasHfCli, parseHfRef, resolveHfDownload, downloadModel, listGgufFiles, getHfModelInfo, isMlxRepo } from "./huggingface.mjs";
|
|
4
|
+
import { hasHfCli, parseHfRef, resolveHfDownload, downloadModel, listGgufFiles, listMmprojFiles, getHfModelInfo, isMlxRepo } from "./huggingface.mjs";
|
|
5
5
|
import { detectHardware, installedRamGB, getFreeDiskBytes } from "./hardware.mjs";
|
|
6
6
|
import { allFittingModels } from "./recommendations.mjs";
|
|
7
7
|
import { parseModelName } from "./model-name.mjs";
|
|
@@ -140,6 +140,22 @@ export async function downloadFlow(prompt) {
|
|
|
140
140
|
return false;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// For GGUF, check if the repo has a vision projector (mmproj) to download alongside
|
|
144
|
+
let extraFiles = [];
|
|
145
|
+
if (plan.format === "gguf") {
|
|
146
|
+
try {
|
|
147
|
+
const mmprojFiles = await listMmprojFiles(repo);
|
|
148
|
+
if (mmprojFiles.length > 0) {
|
|
149
|
+
const mmproj = mmprojFiles[0];
|
|
150
|
+
extraFiles = [mmproj.path];
|
|
151
|
+
plan.totalSizeBytes += mmproj.sizeBytes;
|
|
152
|
+
console.log(pc.dim(`Includes vision projector: ${mmproj.path} (${formatBytes(mmproj.sizeBytes)})`));
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// If we can't check for mmproj, proceed without it
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
143
159
|
console.log(pc.dim(`\nDownloading ${repo}${filename ? `/${filename}` : ""} (${formatBytes(plan.totalSizeBytes)})`));
|
|
144
160
|
if (plan.format === "mlx") {
|
|
145
161
|
const modelParts = repo.split("/").filter(Boolean);
|
|
@@ -158,7 +174,7 @@ export async function downloadFlow(prompt) {
|
|
|
158
174
|
console.log(pc.green("\n✓ Download complete."));
|
|
159
175
|
await offerOmlxRestart(prompt, "to load the new model");
|
|
160
176
|
} else {
|
|
161
|
-
await downloadModel(plan);
|
|
177
|
+
await downloadModel(plan, { extraFiles });
|
|
162
178
|
console.log(pc.green("\n✓ Download complete. Run offgrid-ai again to see the model in the picker."));
|
|
163
179
|
}
|
|
164
180
|
return true;
|
package/src/huggingface.mjs
CHANGED
|
@@ -92,11 +92,11 @@ async function getHfTree(repo, { branch = "main", fetchImpl = globalThis.fetch }
|
|
|
92
92
|
return await response.json();
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
/** List all GGUF files in a HuggingFace repo with their sizes (excludes MTP drafters). */
|
|
95
|
+
/** List all GGUF files in a HuggingFace repo with their sizes (excludes MTP drafters and vision projectors). */
|
|
96
96
|
export async function listGgufFiles(repo, { fetchImpl = globalThis.fetch } = {}) {
|
|
97
97
|
const tree = await getHfTree(repo, { fetchImpl });
|
|
98
98
|
return tree
|
|
99
|
-
.filter((f) => f.type === "file" && f.path.endsWith(".gguf") && !isDrafterFile(f.path))
|
|
99
|
+
.filter((f) => f.type === "file" && f.path.endsWith(".gguf") && !isDrafterFile(f.path) && !isMmprojFile(f.path))
|
|
100
100
|
.map((f) => ({
|
|
101
101
|
path: f.path,
|
|
102
102
|
sizeBytes: f.lfs?.size ?? f.size ?? 0,
|
|
@@ -116,6 +116,23 @@ function isDrafterFile(path) {
|
|
|
116
116
|
return false;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/** Check if a GGUF file is a vision projector (mmproj) based on its name. */
|
|
120
|
+
function isMmprojFile(path) {
|
|
121
|
+
const name = path.split("/").pop() ?? path;
|
|
122
|
+
return /mmproj/i.test(name);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** List all mmproj (vision projector) GGUF files in a HuggingFace repo. */
|
|
126
|
+
export async function listMmprojFiles(repo, { fetchImpl = globalThis.fetch } = {}) {
|
|
127
|
+
const tree = await getHfTree(repo, { fetchImpl });
|
|
128
|
+
return tree
|
|
129
|
+
.filter((f) => f.type === "file" && f.path.endsWith(".gguf") && isMmprojFile(f.path))
|
|
130
|
+
.map((f) => ({
|
|
131
|
+
path: f.path,
|
|
132
|
+
sizeBytes: f.lfs?.size ?? f.size ?? 0,
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
|
|
119
136
|
/** Fetch model metadata from the HF API. */
|
|
120
137
|
export async function getHfModelInfo(repo, { fetchImpl = globalThis.fetch } = {}) {
|
|
121
138
|
const url = `https://huggingface.co/api/models/${repo}`;
|
|
@@ -178,6 +195,7 @@ export async function resolveHfDownload(input, { fetchImpl = globalThis.fetch }
|
|
|
178
195
|
* @param {object} model - from resolveHfDownload
|
|
179
196
|
* @param {object} [options]
|
|
180
197
|
* @param {string} [options.localDir] - for MLX: target directory
|
|
198
|
+
* @param {string[]} [options.extraFiles] - additional files to download (e.g. mmproj)
|
|
181
199
|
* @returns {Promise<{ localDir: string, format: string }>}
|
|
182
200
|
*/
|
|
183
201
|
export async function downloadModel(model, options = {}) {
|
|
@@ -188,6 +206,10 @@ export async function downloadModel(model, options = {}) {
|
|
|
188
206
|
// Single file to HF cache — scanner finds it there
|
|
189
207
|
await mkdir(HF_HUB_DIR, { recursive: true });
|
|
190
208
|
args.push(model.files[0].filename, "--cache-dir", HF_HUB_DIR);
|
|
209
|
+
// Include additional files (e.g. vision projector) in the same download
|
|
210
|
+
for (const file of options.extraFiles ?? []) {
|
|
211
|
+
args.push(file);
|
|
212
|
+
}
|
|
191
213
|
localDir = HF_HUB_DIR;
|
|
192
214
|
} else if (options.localDir) {
|
|
193
215
|
// Full repo to a flat local directory (oMLX)
|