@gmickel/gno 0.3.0
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/README.md +256 -0
- package/assets/skill/SKILL.md +112 -0
- package/assets/skill/cli-reference.md +327 -0
- package/assets/skill/examples.md +234 -0
- package/assets/skill/mcp-reference.md +159 -0
- package/package.json +90 -0
- package/src/app/constants.ts +313 -0
- package/src/cli/colors.ts +65 -0
- package/src/cli/commands/ask.ts +545 -0
- package/src/cli/commands/cleanup.ts +105 -0
- package/src/cli/commands/collection/add.ts +120 -0
- package/src/cli/commands/collection/index.ts +10 -0
- package/src/cli/commands/collection/list.ts +108 -0
- package/src/cli/commands/collection/remove.ts +64 -0
- package/src/cli/commands/collection/rename.ts +95 -0
- package/src/cli/commands/context/add.ts +67 -0
- package/src/cli/commands/context/check.ts +153 -0
- package/src/cli/commands/context/index.ts +10 -0
- package/src/cli/commands/context/list.ts +109 -0
- package/src/cli/commands/context/rm.ts +52 -0
- package/src/cli/commands/doctor.ts +393 -0
- package/src/cli/commands/embed.ts +462 -0
- package/src/cli/commands/get.ts +356 -0
- package/src/cli/commands/index-cmd.ts +119 -0
- package/src/cli/commands/index.ts +102 -0
- package/src/cli/commands/init.ts +328 -0
- package/src/cli/commands/ls.ts +217 -0
- package/src/cli/commands/mcp/config.ts +300 -0
- package/src/cli/commands/mcp/index.ts +24 -0
- package/src/cli/commands/mcp/install.ts +203 -0
- package/src/cli/commands/mcp/paths.ts +470 -0
- package/src/cli/commands/mcp/status.ts +222 -0
- package/src/cli/commands/mcp/uninstall.ts +158 -0
- package/src/cli/commands/mcp.ts +20 -0
- package/src/cli/commands/models/clear.ts +103 -0
- package/src/cli/commands/models/index.ts +32 -0
- package/src/cli/commands/models/list.ts +214 -0
- package/src/cli/commands/models/path.ts +51 -0
- package/src/cli/commands/models/pull.ts +199 -0
- package/src/cli/commands/models/use.ts +85 -0
- package/src/cli/commands/multi-get.ts +400 -0
- package/src/cli/commands/query.ts +220 -0
- package/src/cli/commands/ref-parser.ts +108 -0
- package/src/cli/commands/reset.ts +191 -0
- package/src/cli/commands/search.ts +136 -0
- package/src/cli/commands/shared.ts +156 -0
- package/src/cli/commands/skill/index.ts +19 -0
- package/src/cli/commands/skill/install.ts +197 -0
- package/src/cli/commands/skill/paths-cmd.ts +81 -0
- package/src/cli/commands/skill/paths.ts +191 -0
- package/src/cli/commands/skill/show.ts +73 -0
- package/src/cli/commands/skill/uninstall.ts +141 -0
- package/src/cli/commands/status.ts +205 -0
- package/src/cli/commands/update.ts +68 -0
- package/src/cli/commands/vsearch.ts +188 -0
- package/src/cli/context.ts +64 -0
- package/src/cli/errors.ts +64 -0
- package/src/cli/format/search-results.ts +211 -0
- package/src/cli/options.ts +183 -0
- package/src/cli/program.ts +1330 -0
- package/src/cli/run.ts +213 -0
- package/src/cli/ui.ts +92 -0
- package/src/config/defaults.ts +20 -0
- package/src/config/index.ts +55 -0
- package/src/config/loader.ts +161 -0
- package/src/config/paths.ts +87 -0
- package/src/config/saver.ts +153 -0
- package/src/config/types.ts +280 -0
- package/src/converters/adapters/markitdownTs/adapter.ts +140 -0
- package/src/converters/adapters/officeparser/adapter.ts +126 -0
- package/src/converters/canonicalize.ts +89 -0
- package/src/converters/errors.ts +218 -0
- package/src/converters/index.ts +51 -0
- package/src/converters/mime.ts +163 -0
- package/src/converters/native/markdown.ts +115 -0
- package/src/converters/native/plaintext.ts +56 -0
- package/src/converters/path.ts +48 -0
- package/src/converters/pipeline.ts +159 -0
- package/src/converters/registry.ts +74 -0
- package/src/converters/types.ts +123 -0
- package/src/converters/versions.ts +24 -0
- package/src/index.ts +27 -0
- package/src/ingestion/chunker.ts +238 -0
- package/src/ingestion/index.ts +32 -0
- package/src/ingestion/language.ts +276 -0
- package/src/ingestion/sync.ts +671 -0
- package/src/ingestion/types.ts +219 -0
- package/src/ingestion/walker.ts +235 -0
- package/src/llm/cache.ts +467 -0
- package/src/llm/errors.ts +191 -0
- package/src/llm/index.ts +58 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +133 -0
- package/src/llm/nodeLlamaCpp/embedding.ts +165 -0
- package/src/llm/nodeLlamaCpp/generation.ts +88 -0
- package/src/llm/nodeLlamaCpp/lifecycle.ts +317 -0
- package/src/llm/nodeLlamaCpp/rerank.ts +94 -0
- package/src/llm/registry.ts +86 -0
- package/src/llm/types.ts +129 -0
- package/src/mcp/resources/index.ts +151 -0
- package/src/mcp/server.ts +229 -0
- package/src/mcp/tools/get.ts +220 -0
- package/src/mcp/tools/index.ts +160 -0
- package/src/mcp/tools/multi-get.ts +263 -0
- package/src/mcp/tools/query.ts +226 -0
- package/src/mcp/tools/search.ts +119 -0
- package/src/mcp/tools/status.ts +81 -0
- package/src/mcp/tools/vsearch.ts +198 -0
- package/src/pipeline/chunk-lookup.ts +44 -0
- package/src/pipeline/expansion.ts +256 -0
- package/src/pipeline/explain.ts +115 -0
- package/src/pipeline/fusion.ts +185 -0
- package/src/pipeline/hybrid.ts +535 -0
- package/src/pipeline/index.ts +64 -0
- package/src/pipeline/query-language.ts +118 -0
- package/src/pipeline/rerank.ts +223 -0
- package/src/pipeline/search.ts +261 -0
- package/src/pipeline/types.ts +328 -0
- package/src/pipeline/vsearch.ts +348 -0
- package/src/store/index.ts +41 -0
- package/src/store/migrations/001-initial.ts +196 -0
- package/src/store/migrations/index.ts +20 -0
- package/src/store/migrations/runner.ts +187 -0
- package/src/store/sqlite/adapter.ts +1242 -0
- package/src/store/sqlite/index.ts +7 -0
- package/src/store/sqlite/setup.ts +129 -0
- package/src/store/sqlite/types.ts +28 -0
- package/src/store/types.ts +506 -0
- package/src/store/vector/index.ts +13 -0
- package/src/store/vector/sqlite-vec.ts +373 -0
- package/src/store/vector/stats.ts +152 -0
- package/src/store/vector/types.ts +115 -0
package/src/llm/cache.ts
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model cache resolver.
|
|
3
|
+
* Handles hf: URI parsing and model cache management.
|
|
4
|
+
*
|
|
5
|
+
* @module src/llm/cache
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
9
|
+
// node:path: join for path construction, isAbsolute for cross-platform path detection
|
|
10
|
+
import { isAbsolute, join } from 'node:path';
|
|
11
|
+
// node:url: fileURLToPath for proper file:// URL handling
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { getModelsCachePath } from '../app/constants';
|
|
14
|
+
import {
|
|
15
|
+
downloadFailedError,
|
|
16
|
+
invalidUriError,
|
|
17
|
+
modelNotCachedError,
|
|
18
|
+
modelNotFoundError,
|
|
19
|
+
} from './errors';
|
|
20
|
+
import type {
|
|
21
|
+
DownloadProgress,
|
|
22
|
+
LlmResult,
|
|
23
|
+
ModelCacheEntry,
|
|
24
|
+
ModelType,
|
|
25
|
+
ProgressCallback,
|
|
26
|
+
} from './types';
|
|
27
|
+
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
// URI Parsing
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
// Regex patterns for URI parsing (top-level for performance)
|
|
33
|
+
const HF_QUANT_PATTERN = /^([^/]+)\/([^/:]+):(\w+)$/;
|
|
34
|
+
const HF_PATH_PATTERN = /^([^/]+)\/([^/]+)\/(.+\.gguf)$/;
|
|
35
|
+
|
|
36
|
+
export type ParsedModelUri =
|
|
37
|
+
| {
|
|
38
|
+
scheme: 'hf';
|
|
39
|
+
org: string;
|
|
40
|
+
repo: string;
|
|
41
|
+
file: string;
|
|
42
|
+
quantization?: string;
|
|
43
|
+
}
|
|
44
|
+
| {
|
|
45
|
+
scheme: 'file';
|
|
46
|
+
file: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse a model URI into components.
|
|
51
|
+
*
|
|
52
|
+
* Supported formats:
|
|
53
|
+
* - hf:org/repo/file.gguf (explicit file)
|
|
54
|
+
* - hf:org/repo:Q4_K_M (quantization shorthand - infers filename)
|
|
55
|
+
* - file:///path/to/model.gguf (standard file URL)
|
|
56
|
+
* - file:///C:/path/to/model.gguf (Windows file URL)
|
|
57
|
+
* - file:/path/to/model.gguf (simplified file URI)
|
|
58
|
+
* - /path/to/model.gguf (Unix absolute path)
|
|
59
|
+
* - C:\path\to\model.gguf (Windows absolute path)
|
|
60
|
+
* - \\server\share\model.gguf (UNC path)
|
|
61
|
+
*/
|
|
62
|
+
export function parseModelUri(
|
|
63
|
+
uri: string
|
|
64
|
+
): { ok: true; value: ParsedModelUri } | { ok: false; error: string } {
|
|
65
|
+
// Handle hf: scheme
|
|
66
|
+
if (uri.startsWith('hf:')) {
|
|
67
|
+
const rest = uri.slice(3);
|
|
68
|
+
|
|
69
|
+
// Check for quantization shorthand: hf:org/repo:Q4_K_M
|
|
70
|
+
const colonMatch = rest.match(HF_QUANT_PATTERN);
|
|
71
|
+
if (colonMatch) {
|
|
72
|
+
const [, org, repo, quant] = colonMatch;
|
|
73
|
+
// Regex guarantees these are defined when match succeeds
|
|
74
|
+
if (org && repo && quant) {
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
value: {
|
|
78
|
+
scheme: 'hf',
|
|
79
|
+
org,
|
|
80
|
+
repo,
|
|
81
|
+
file: '', // Will be resolved by node-llama-cpp
|
|
82
|
+
quantization: quant,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Full path: hf:org/repo/file.gguf
|
|
89
|
+
const pathMatch = rest.match(HF_PATH_PATTERN);
|
|
90
|
+
if (pathMatch) {
|
|
91
|
+
const [, org, repo, file] = pathMatch;
|
|
92
|
+
// Regex guarantees these are defined when match succeeds
|
|
93
|
+
if (org && repo && file) {
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
value: {
|
|
97
|
+
scheme: 'hf',
|
|
98
|
+
org,
|
|
99
|
+
repo,
|
|
100
|
+
file,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { ok: false, error: `Invalid hf: URI format: ${uri}` };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Handle file:// URLs (proper file URLs like file:///C:/path or file:///path)
|
|
110
|
+
if (uri.startsWith('file://')) {
|
|
111
|
+
try {
|
|
112
|
+
const filePath = fileURLToPath(new URL(uri));
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
value: { scheme: 'file', file: filePath },
|
|
116
|
+
};
|
|
117
|
+
} catch {
|
|
118
|
+
return { ok: false, error: `Invalid file URL: ${uri}` };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle simplified file: scheme (file:/path or file:C:\path)
|
|
123
|
+
if (uri.startsWith('file:')) {
|
|
124
|
+
const path = uri.slice(5);
|
|
125
|
+
if (!path) {
|
|
126
|
+
return { ok: false, error: 'Empty file path' };
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
ok: true,
|
|
130
|
+
value: { scheme: 'file', file: path },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Treat as local file path if absolute (works on both Unix and Windows)
|
|
135
|
+
if (isAbsolute(uri)) {
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
value: { scheme: 'file', file: uri },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { ok: false, error: `Unknown URI scheme: ${uri}` };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Convert parsed URI back to node-llama-cpp format.
|
|
147
|
+
*/
|
|
148
|
+
export function toNodeLlamaCppUri(parsed: ParsedModelUri): string {
|
|
149
|
+
if (parsed.scheme === 'file') {
|
|
150
|
+
return parsed.file;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// hf: format for node-llama-cpp
|
|
154
|
+
if (parsed.quantization) {
|
|
155
|
+
return `hf:${parsed.org}/${parsed.repo}:${parsed.quantization}`;
|
|
156
|
+
}
|
|
157
|
+
return `hf:${parsed.org}/${parsed.repo}/${parsed.file}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
161
|
+
// Manifest
|
|
162
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
interface Manifest {
|
|
165
|
+
version: '1.0';
|
|
166
|
+
models: ModelCacheEntry[];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const MANIFEST_VERSION = '1.0' as const;
|
|
170
|
+
|
|
171
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
172
|
+
// ModelCache
|
|
173
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
export class ModelCache {
|
|
176
|
+
readonly dir: string;
|
|
177
|
+
private readonly manifestPath: string;
|
|
178
|
+
private manifest: Manifest | null = null;
|
|
179
|
+
|
|
180
|
+
constructor(cacheDir?: string) {
|
|
181
|
+
this.dir = cacheDir ?? getModelsCachePath();
|
|
182
|
+
this.manifestPath = join(this.dir, 'manifest.json');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resolve a model URI to a local file path.
|
|
187
|
+
* Returns error if not cached.
|
|
188
|
+
*/
|
|
189
|
+
async resolve(uri: string, type: ModelType): Promise<LlmResult<string>> {
|
|
190
|
+
const parsed = parseModelUri(uri);
|
|
191
|
+
if (!parsed.ok) {
|
|
192
|
+
return { ok: false, error: invalidUriError(uri, parsed.error) };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Local files: verify existence
|
|
196
|
+
if (parsed.value.scheme === 'file') {
|
|
197
|
+
const exists = await this.fileExists(parsed.value.file);
|
|
198
|
+
if (!exists) {
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
error: modelNotFoundError(
|
|
202
|
+
uri,
|
|
203
|
+
`File not found: ${parsed.value.file}`
|
|
204
|
+
),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return { ok: true, value: parsed.value.file };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// HF models: check cache
|
|
211
|
+
const cached = await this.getCachedPath(uri);
|
|
212
|
+
if (cached) {
|
|
213
|
+
return { ok: true, value: cached };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { ok: false, error: modelNotCachedError(uri, type) };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Download a model to the cache.
|
|
221
|
+
* Uses node-llama-cpp's resolveModelFile for HF models.
|
|
222
|
+
*/
|
|
223
|
+
async download(
|
|
224
|
+
uri: string,
|
|
225
|
+
type: ModelType,
|
|
226
|
+
onProgress?: ProgressCallback,
|
|
227
|
+
force?: boolean
|
|
228
|
+
): Promise<LlmResult<string>> {
|
|
229
|
+
const parsed = parseModelUri(uri);
|
|
230
|
+
if (!parsed.ok) {
|
|
231
|
+
return { ok: false, error: invalidUriError(uri, parsed.error) };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Local files: just verify
|
|
235
|
+
if (parsed.value.scheme === 'file') {
|
|
236
|
+
const exists = await this.fileExists(parsed.value.file);
|
|
237
|
+
if (!exists) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
error: modelNotFoundError(
|
|
241
|
+
uri,
|
|
242
|
+
`File not found: ${parsed.value.file}`
|
|
243
|
+
),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return { ok: true, value: parsed.value.file };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Ensure cache dir exists
|
|
250
|
+
await mkdir(this.dir, { recursive: true });
|
|
251
|
+
|
|
252
|
+
// Force: delete existing file to trigger re-download
|
|
253
|
+
if (force) {
|
|
254
|
+
const existingPath = await this.getCachedPath(uri);
|
|
255
|
+
if (existingPath) {
|
|
256
|
+
await rm(existingPath).catch(() => {
|
|
257
|
+
// Ignore: file may not exist or already deleted
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const { resolveModelFile } = await import('node-llama-cpp');
|
|
264
|
+
|
|
265
|
+
// Convert to node-llama-cpp format (handles quantization shorthand)
|
|
266
|
+
// node-llama-cpp needs hf: prefix to identify HuggingFace models
|
|
267
|
+
const hfUri = toNodeLlamaCppUri(parsed.value);
|
|
268
|
+
|
|
269
|
+
const resolvedPath = await resolveModelFile(hfUri, {
|
|
270
|
+
directory: this.dir,
|
|
271
|
+
onProgress: onProgress
|
|
272
|
+
? (status: unknown) => {
|
|
273
|
+
// Type-safe check for download progress status
|
|
274
|
+
if (
|
|
275
|
+
status &&
|
|
276
|
+
typeof status === 'object' &&
|
|
277
|
+
'type' in status &&
|
|
278
|
+
status.type === 'download' &&
|
|
279
|
+
'downloadedSize' in status &&
|
|
280
|
+
'totalSize' in status
|
|
281
|
+
) {
|
|
282
|
+
const s = status as {
|
|
283
|
+
downloadedSize: number;
|
|
284
|
+
totalSize: number;
|
|
285
|
+
};
|
|
286
|
+
const progress: DownloadProgress = {
|
|
287
|
+
downloadedBytes: s.downloadedSize,
|
|
288
|
+
totalBytes: s.totalSize,
|
|
289
|
+
percent:
|
|
290
|
+
s.totalSize > 0
|
|
291
|
+
? (s.downloadedSize / s.totalSize) * 100
|
|
292
|
+
: 0,
|
|
293
|
+
};
|
|
294
|
+
onProgress(progress);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
: undefined,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Update manifest
|
|
301
|
+
await this.addToManifest(uri, type, resolvedPath);
|
|
302
|
+
|
|
303
|
+
return { ok: true, value: resolvedPath };
|
|
304
|
+
} catch (e) {
|
|
305
|
+
return { ok: false, error: downloadFailedError(uri, e) };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Check if a model is cached/available.
|
|
311
|
+
* For file: URIs, checks if file exists on disk.
|
|
312
|
+
* For hf: URIs, checks the manifest.
|
|
313
|
+
*/
|
|
314
|
+
async isCached(uri: string): Promise<boolean> {
|
|
315
|
+
const cached = await this.getCachedPath(uri);
|
|
316
|
+
return cached !== null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get cached/available path for a URI.
|
|
321
|
+
* For file: URIs, returns path if file exists.
|
|
322
|
+
* For hf: URIs, checks the manifest.
|
|
323
|
+
*/
|
|
324
|
+
async getCachedPath(uri: string): Promise<string | null> {
|
|
325
|
+
// Handle file: URIs directly (check filesystem, not manifest)
|
|
326
|
+
const parsed = parseModelUri(uri);
|
|
327
|
+
if (parsed.ok && parsed.value.scheme === 'file') {
|
|
328
|
+
const exists = await this.fileExists(parsed.value.file);
|
|
329
|
+
return exists ? parsed.value.file : null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// HF URIs: check manifest
|
|
333
|
+
const manifest = await this.loadManifest();
|
|
334
|
+
const entry = manifest.models.find((m) => m.uri === uri);
|
|
335
|
+
if (!entry) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Verify file still exists
|
|
340
|
+
const exists = await this.fileExists(entry.path);
|
|
341
|
+
if (!exists) {
|
|
342
|
+
// Remove stale entry
|
|
343
|
+
await this.removeFromManifest(uri);
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return entry.path;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* List all cached models.
|
|
352
|
+
*/
|
|
353
|
+
async list(): Promise<ModelCacheEntry[]> {
|
|
354
|
+
const manifest = await this.loadManifest();
|
|
355
|
+
return manifest.models;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get total size of all cached models.
|
|
360
|
+
*/
|
|
361
|
+
async totalSize(): Promise<number> {
|
|
362
|
+
const manifest = await this.loadManifest();
|
|
363
|
+
return manifest.models.reduce((sum, m) => sum + (m.size || 0), 0);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Clear cached models.
|
|
368
|
+
* If types provided, only clears models of those types.
|
|
369
|
+
*/
|
|
370
|
+
async clear(types?: ModelType[]): Promise<void> {
|
|
371
|
+
const manifest = await this.loadManifest();
|
|
372
|
+
|
|
373
|
+
const toRemove = types
|
|
374
|
+
? manifest.models.filter((m) => types.includes(m.type))
|
|
375
|
+
: manifest.models;
|
|
376
|
+
|
|
377
|
+
for (const entry of toRemove) {
|
|
378
|
+
try {
|
|
379
|
+
await rm(entry.path, { force: true });
|
|
380
|
+
} catch {
|
|
381
|
+
// Ignore deletion errors
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Update manifest
|
|
386
|
+
if (types) {
|
|
387
|
+
manifest.models = manifest.models.filter((m) => !types.includes(m.type));
|
|
388
|
+
} else {
|
|
389
|
+
manifest.models = [];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
await this.saveManifest(manifest);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
396
|
+
// Private
|
|
397
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
private async fileExists(path: string): Promise<boolean> {
|
|
400
|
+
try {
|
|
401
|
+
await stat(path);
|
|
402
|
+
return true;
|
|
403
|
+
} catch {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private async loadManifest(): Promise<Manifest> {
|
|
409
|
+
if (this.manifest) {
|
|
410
|
+
return this.manifest;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const content = await readFile(this.manifestPath, 'utf-8');
|
|
415
|
+
this.manifest = JSON.parse(content) as Manifest;
|
|
416
|
+
return this.manifest;
|
|
417
|
+
} catch {
|
|
418
|
+
// No manifest or invalid - create empty
|
|
419
|
+
this.manifest = { version: MANIFEST_VERSION, models: [] };
|
|
420
|
+
return this.manifest;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private async saveManifest(manifest: Manifest): Promise<void> {
|
|
425
|
+
await mkdir(this.dir, { recursive: true });
|
|
426
|
+
await writeFile(this.manifestPath, JSON.stringify(manifest, null, 2));
|
|
427
|
+
this.manifest = manifest;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private async addToManifest(
|
|
431
|
+
uri: string,
|
|
432
|
+
type: ModelType,
|
|
433
|
+
path: string
|
|
434
|
+
): Promise<void> {
|
|
435
|
+
const manifest = await this.loadManifest();
|
|
436
|
+
|
|
437
|
+
// Get file size and compute checksum
|
|
438
|
+
let size = 0;
|
|
439
|
+
try {
|
|
440
|
+
const stats = await stat(path);
|
|
441
|
+
size = stats.size;
|
|
442
|
+
} catch {
|
|
443
|
+
// Ignore
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Remove existing entry if present
|
|
447
|
+
manifest.models = manifest.models.filter((m) => m.uri !== uri);
|
|
448
|
+
|
|
449
|
+
// Add new entry
|
|
450
|
+
manifest.models.push({
|
|
451
|
+
uri,
|
|
452
|
+
type,
|
|
453
|
+
path,
|
|
454
|
+
size,
|
|
455
|
+
checksum: '', // TODO: compute SHA-256 for large files
|
|
456
|
+
cachedAt: new Date().toISOString(),
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
await this.saveManifest(manifest);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private async removeFromManifest(uri: string): Promise<void> {
|
|
463
|
+
const manifest = await this.loadManifest();
|
|
464
|
+
manifest.models = manifest.models.filter((m) => m.uri !== uri);
|
|
465
|
+
await this.saveManifest(manifest);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM error types and helpers.
|
|
3
|
+
* Follows the pattern from converters/errors.ts
|
|
4
|
+
*
|
|
5
|
+
* @module src/llm/errors
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Error Types
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export type LlmErrorCode =
|
|
13
|
+
| 'MODEL_NOT_FOUND'
|
|
14
|
+
| 'MODEL_NOT_CACHED'
|
|
15
|
+
| 'MODEL_DOWNLOAD_FAILED'
|
|
16
|
+
| 'MODEL_LOAD_FAILED'
|
|
17
|
+
| 'MODEL_CORRUPTED'
|
|
18
|
+
| 'INFERENCE_FAILED'
|
|
19
|
+
| 'TIMEOUT'
|
|
20
|
+
| 'OUT_OF_MEMORY'
|
|
21
|
+
| 'INVALID_URI';
|
|
22
|
+
|
|
23
|
+
export interface LlmError {
|
|
24
|
+
code: LlmErrorCode;
|
|
25
|
+
message: string;
|
|
26
|
+
modelUri?: string;
|
|
27
|
+
retryable: boolean;
|
|
28
|
+
cause?: unknown;
|
|
29
|
+
suggestion?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
// Constants
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const MAX_CAUSE_LENGTH = 1000;
|
|
37
|
+
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
// Helpers
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalize a cause to safe, serializable format.
|
|
44
|
+
*/
|
|
45
|
+
function normalizeCause(
|
|
46
|
+
cause: unknown
|
|
47
|
+
): { name: string; message: string } | string | undefined {
|
|
48
|
+
if (cause === undefined || cause === null) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (cause instanceof Error) {
|
|
53
|
+
const message =
|
|
54
|
+
cause.message.length > MAX_CAUSE_LENGTH
|
|
55
|
+
? `${cause.message.slice(0, MAX_CAUSE_LENGTH)}...`
|
|
56
|
+
: cause.message;
|
|
57
|
+
return { name: cause.name, message };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof cause === 'string') {
|
|
61
|
+
return cause.length > MAX_CAUSE_LENGTH
|
|
62
|
+
? `${cause.slice(0, MAX_CAUSE_LENGTH)}...`
|
|
63
|
+
: cause;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const str = String(cause);
|
|
68
|
+
return str.length > MAX_CAUSE_LENGTH
|
|
69
|
+
? `${str.slice(0, MAX_CAUSE_LENGTH)}...`
|
|
70
|
+
: str;
|
|
71
|
+
} catch {
|
|
72
|
+
return '[unserializable cause]';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create an LlmError with normalized cause.
|
|
78
|
+
*/
|
|
79
|
+
export function llmError(
|
|
80
|
+
code: LlmErrorCode,
|
|
81
|
+
opts: Omit<LlmError, 'code'>
|
|
82
|
+
): LlmError {
|
|
83
|
+
return {
|
|
84
|
+
code,
|
|
85
|
+
...opts,
|
|
86
|
+
cause: normalizeCause(opts.cause),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if error is retryable.
|
|
92
|
+
*/
|
|
93
|
+
export function isRetryable(code: LlmErrorCode): boolean {
|
|
94
|
+
return ['MODEL_DOWNLOAD_FAILED', 'TIMEOUT', 'INFERENCE_FAILED'].includes(
|
|
95
|
+
code
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
// Error Factories
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export function modelNotFoundError(uri: string, details?: string): LlmError {
|
|
104
|
+
return llmError('MODEL_NOT_FOUND', {
|
|
105
|
+
message: details
|
|
106
|
+
? `Model not found: ${details}`
|
|
107
|
+
: `Model not found: ${uri}`,
|
|
108
|
+
modelUri: uri,
|
|
109
|
+
retryable: false,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function modelNotCachedError(
|
|
114
|
+
uri: string,
|
|
115
|
+
modelType: 'embed' | 'rerank' | 'gen'
|
|
116
|
+
): LlmError {
|
|
117
|
+
return llmError('MODEL_NOT_CACHED', {
|
|
118
|
+
message: `${modelType} model not cached`,
|
|
119
|
+
modelUri: uri,
|
|
120
|
+
retryable: false,
|
|
121
|
+
suggestion: `Run: gno models pull --${modelType}`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function downloadFailedError(uri: string, cause?: unknown): LlmError {
|
|
126
|
+
return llmError('MODEL_DOWNLOAD_FAILED', {
|
|
127
|
+
message: `Failed to download model: ${uri}`,
|
|
128
|
+
modelUri: uri,
|
|
129
|
+
retryable: true,
|
|
130
|
+
cause,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function loadFailedError(uri: string, cause?: unknown): LlmError {
|
|
135
|
+
return llmError('MODEL_LOAD_FAILED', {
|
|
136
|
+
message: `Failed to load model: ${uri}`,
|
|
137
|
+
modelUri: uri,
|
|
138
|
+
retryable: false,
|
|
139
|
+
cause,
|
|
140
|
+
suggestion: 'Run: gno doctor',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function corruptedError(uri: string, cause?: unknown): LlmError {
|
|
145
|
+
return llmError('MODEL_CORRUPTED', {
|
|
146
|
+
message: `Model file corrupted: ${uri}`,
|
|
147
|
+
modelUri: uri,
|
|
148
|
+
retryable: false,
|
|
149
|
+
cause,
|
|
150
|
+
suggestion: 'Run: gno models clear && gno models pull',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function inferenceFailedError(uri: string, cause?: unknown): LlmError {
|
|
155
|
+
return llmError('INFERENCE_FAILED', {
|
|
156
|
+
message: `Inference failed for model: ${uri}`,
|
|
157
|
+
modelUri: uri,
|
|
158
|
+
retryable: true,
|
|
159
|
+
cause,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function timeoutError(
|
|
164
|
+
uri: string,
|
|
165
|
+
operation: 'load' | 'inference',
|
|
166
|
+
timeoutMs: number
|
|
167
|
+
): LlmError {
|
|
168
|
+
return llmError('TIMEOUT', {
|
|
169
|
+
message: `${operation} timed out after ${timeoutMs}ms`,
|
|
170
|
+
modelUri: uri,
|
|
171
|
+
retryable: true,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function outOfMemoryError(uri: string, cause?: unknown): LlmError {
|
|
176
|
+
return llmError('OUT_OF_MEMORY', {
|
|
177
|
+
message: `Out of memory loading model: ${uri}`,
|
|
178
|
+
modelUri: uri,
|
|
179
|
+
retryable: false,
|
|
180
|
+
cause,
|
|
181
|
+
suggestion: 'Try a smaller quantization (Q4_K_M) or close other apps',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function invalidUriError(uri: string, details: string): LlmError {
|
|
186
|
+
return llmError('INVALID_URI', {
|
|
187
|
+
message: `Invalid model URI: ${details}`,
|
|
188
|
+
modelUri: uri,
|
|
189
|
+
retryable: false,
|
|
190
|
+
});
|
|
191
|
+
}
|
package/src/llm/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM subsystem public API.
|
|
3
|
+
*
|
|
4
|
+
* @module src/llm
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Re-export config types (source of truth in config/types.ts)
|
|
8
|
+
export type { ModelConfig, ModelPreset } from '../config/types';
|
|
9
|
+
export type { ParsedModelUri } from './cache';
|
|
10
|
+
// Cache
|
|
11
|
+
export { ModelCache, parseModelUri, toNodeLlamaCppUri } from './cache';
|
|
12
|
+
// Errors
|
|
13
|
+
export type { LlmError, LlmErrorCode } from './errors';
|
|
14
|
+
export {
|
|
15
|
+
corruptedError,
|
|
16
|
+
downloadFailedError,
|
|
17
|
+
inferenceFailedError,
|
|
18
|
+
invalidUriError,
|
|
19
|
+
isRetryable,
|
|
20
|
+
llmError,
|
|
21
|
+
loadFailedError,
|
|
22
|
+
modelNotCachedError,
|
|
23
|
+
modelNotFoundError,
|
|
24
|
+
outOfMemoryError,
|
|
25
|
+
timeoutError,
|
|
26
|
+
} from './errors';
|
|
27
|
+
// Adapter
|
|
28
|
+
export { createLlmAdapter, LlmAdapter } from './nodeLlamaCpp/adapter';
|
|
29
|
+
// Lifecycle
|
|
30
|
+
export {
|
|
31
|
+
getModelManager,
|
|
32
|
+
ModelManager,
|
|
33
|
+
resetModelManager,
|
|
34
|
+
} from './nodeLlamaCpp/lifecycle';
|
|
35
|
+
// Registry
|
|
36
|
+
export {
|
|
37
|
+
getActivePreset,
|
|
38
|
+
getModelConfig,
|
|
39
|
+
getPreset,
|
|
40
|
+
listPresets,
|
|
41
|
+
resolveModelUri,
|
|
42
|
+
} from './registry';
|
|
43
|
+
// Types
|
|
44
|
+
export type {
|
|
45
|
+
DownloadProgress,
|
|
46
|
+
EmbeddingPort,
|
|
47
|
+
GenerationPort,
|
|
48
|
+
GenParams,
|
|
49
|
+
LlmResult,
|
|
50
|
+
LoadedModel,
|
|
51
|
+
ModelCacheEntry,
|
|
52
|
+
ModelStatus,
|
|
53
|
+
ModelType,
|
|
54
|
+
ModelUri,
|
|
55
|
+
ProgressCallback,
|
|
56
|
+
RerankPort,
|
|
57
|
+
RerankScore,
|
|
58
|
+
} from './types';
|