@qvac/harness 0.3.2 → 0.4.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/CHANGELOG.md +56 -1
- package/README.md +134 -0
- package/dist/harness.bundle +304 -175
- package/dist-lib/index.d.ts +6 -1
- package/dist-lib/index.js +29 -4
- package/dist-lib/lib/capabilities/completion.js +2 -5
- package/dist-lib/lib/capabilities/preview-model-source.d.ts +67 -0
- package/dist-lib/lib/capabilities/preview-model-source.js +387 -0
- package/dist-lib/lib/connect.d.ts +4 -5
- package/dist-lib/lib/connect.js +9 -0
- package/dist-lib/lib/contract-types.d.ts +4 -5
- package/dist-lib/lib/models/custom-model-cache.d.ts +4 -0
- package/dist-lib/lib/models/custom-model-cache.js +42 -0
- package/dist-lib/lib/models/gguf-header.d.ts +30 -0
- package/dist-lib/lib/models/gguf-header.js +208 -0
- package/dist-lib/lib/models/model-catalog.d.ts +6 -1
- package/dist-lib/lib/models/model-catalog.js +7 -2
- package/dist-lib/lib/models/model-delete.d.ts +4 -1
- package/dist-lib/lib/models/model-delete.js +19 -14
- package/dist-lib/lib/models/model-list.d.ts +6 -0
- package/dist-lib/lib/models/model-list.js +23 -1
- package/dist-lib/lib/models/model-source.js +1 -1
- package/dist-lib/lib/serve.js +21 -5
- package/dist-lib/schema/harness.js +42 -1
- package/dist-lib/spec/bind.js +3 -0
- package/dist-lib/spec/calls.js +1 -0
- package/dist-lib/spec/hrpc/hrpc.json +13 -0
- package/dist-lib/spec/hrpc/index.js +15 -3
- package/dist-lib/spec/hrpc/messages.js +173 -5
- package/dist-lib/spec/hyperschema/index.js +173 -5
- package/dist-lib/spec/hyperschema/schema.json +136 -1
- package/dist-lib/spec/hyperschema/types.d.ts +34 -1
- package/package.json +6 -7
- package/worker/harness-sidecar.mjs +1 -0
- package/dist-lib/lib/capabilities/vision-history.d.ts +0 -3
- package/dist-lib/lib/capabilities/vision-history.js +0 -21
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { HarnessCapabilityProfile } from '../spec/hyperschema/types.d.ts';
|
|
2
2
|
import type { ApproveFn, AskUserFn, AttachFn, FetchAttachmentFn, ProgressFn, RagSearchFn, ResourceDescriptor, ResponseFormat } from './capabilities/completion.ts';
|
|
3
3
|
import type { KvCacheDeletionResult } from './capabilities/kv-cache.ts';
|
|
4
|
+
import type { ModelSourcePreview, PreviewModelSourceContext } from './capabilities/preview-model-source.ts';
|
|
4
5
|
import type { MemoryBudget } from './hardware/memory-budget.ts';
|
|
5
6
|
import type { EmbedDevice } from './models/embed-config.ts';
|
|
6
7
|
import type { LlmGenerationParams } from './models/llm-presets.ts';
|
|
8
|
+
import type { CustomModelEntry } from './models/model-catalog.ts';
|
|
7
9
|
import type { DeleteModelSummary } from './models/model-delete.ts';
|
|
8
10
|
import type { ListModelsOptions, ModelInfo } from './models/model-list.ts';
|
|
9
11
|
import type { ToolCatalogEntry } from './tools/catalog.ts';
|
|
@@ -219,11 +221,8 @@ export interface HarnessContract {
|
|
|
219
221
|
name: string;
|
|
220
222
|
addon: string;
|
|
221
223
|
}>>;
|
|
222
|
-
setCustomModels?(entries: ReadonlyArray<
|
|
223
|
-
|
|
224
|
-
src: string;
|
|
225
|
-
projectionSrc?: string;
|
|
226
|
-
}>): void | Promise<void>;
|
|
224
|
+
setCustomModels?(entries: ReadonlyArray<CustomModelEntry>): void | Promise<void>;
|
|
225
|
+
previewModelSource?(context: PreviewModelSourceContext): Promise<ModelSourcePreview>;
|
|
227
226
|
capability?(): Promise<CapabilityInfo>;
|
|
228
227
|
diagnostics?(): HarnessDiagnostics;
|
|
229
228
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CustomModelEntry } from './model-catalog.ts';
|
|
2
|
+
export declare function customFileCachePath(src: string, modelsDir: string | undefined): string | null;
|
|
3
|
+
export declare function downloadedCustomFiles(entry: CustomModelEntry, modelsDir: string | undefined): string[];
|
|
4
|
+
export declare function isCustomModelCached(entry: CustomModelEntry, modelsDir: string | undefined): Promise<boolean>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { isRemoteSource, localSourcePath } from './gguf-header.js';
|
|
5
|
+
const SHARD = /-\d{5}-of-\d{5}\.\w+$/;
|
|
6
|
+
const ARCHIVE = /\.(tar|tgz|zip)(\.\w+)?$/i;
|
|
7
|
+
export function customFileCachePath(src, modelsDir) {
|
|
8
|
+
if (!isRemoteSource(src))
|
|
9
|
+
return localSourcePath(src);
|
|
10
|
+
const filename = src.split('/').pop()?.split('?')[0] || 'model.gguf';
|
|
11
|
+
if (!modelsDir || SHARD.test(filename) || ARCHIVE.test(filename))
|
|
12
|
+
return null;
|
|
13
|
+
const hash = crypto.createHash('sha256').update(src, 'utf8').digest('hex').slice(0, 16);
|
|
14
|
+
return path.join(modelsDir, `${hash}_${filename}`);
|
|
15
|
+
}
|
|
16
|
+
export function downloadedCustomFiles(entry, modelsDir) {
|
|
17
|
+
const srcs = entry.projectionSrc ? [entry.src, entry.projectionSrc] : [entry.src];
|
|
18
|
+
return srcs
|
|
19
|
+
.filter(isRemoteSource)
|
|
20
|
+
.map((src) => customFileCachePath(src, modelsDir))
|
|
21
|
+
.filter((file) => file !== null);
|
|
22
|
+
}
|
|
23
|
+
export async function isCustomModelCached(entry, modelsDir) {
|
|
24
|
+
const srcs = entry.projectionSrc ? [entry.src, entry.projectionSrc] : [entry.src];
|
|
25
|
+
const sizes = await Promise.all(srcs.map((src) => onDiskSize(customFileCachePath(src, modelsDir))));
|
|
26
|
+
const present = sizes.filter((size) => size !== null);
|
|
27
|
+
if (present.length < sizes.length)
|
|
28
|
+
return false;
|
|
29
|
+
const total = present.reduce((sum, size) => sum + size, 0);
|
|
30
|
+
return entry.expectedSize ? total === entry.expectedSize : true;
|
|
31
|
+
}
|
|
32
|
+
async function onDiskSize(file) {
|
|
33
|
+
if (file === null)
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
const stat = await fs.promises.stat(file);
|
|
37
|
+
return stat.isFile() ? stat.size : null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type AbortSignal } from 'bare-abort-controller';
|
|
2
|
+
export interface GgufHeader {
|
|
3
|
+
readonly architecture?: string;
|
|
4
|
+
readonly name?: string;
|
|
5
|
+
readonly type?: string;
|
|
6
|
+
readonly license?: string;
|
|
7
|
+
readonly contextLength?: number;
|
|
8
|
+
readonly sizeLabel?: string;
|
|
9
|
+
readonly tags?: ReadonlyArray<string>;
|
|
10
|
+
}
|
|
11
|
+
export type GgufRangeFetch = (url: string, init: {
|
|
12
|
+
headers: Record<string, string>;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}) => Promise<{
|
|
15
|
+
ok: boolean;
|
|
16
|
+
status: number;
|
|
17
|
+
headers: {
|
|
18
|
+
get(name: string): string | null;
|
|
19
|
+
};
|
|
20
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
21
|
+
}>;
|
|
22
|
+
export declare const GGUF_HEADER_BYTES = 1048576;
|
|
23
|
+
export declare const GGUF_HEADER_TIMEOUT_MS = 30000;
|
|
24
|
+
export declare function isRemoteSource(src: string): boolean;
|
|
25
|
+
export declare function localSourcePath(src: string): string | null;
|
|
26
|
+
export declare function readGgufHeaderBytes(src: string, opts?: {
|
|
27
|
+
fetch?: GgufRangeFetch;
|
|
28
|
+
maxBytes?: number;
|
|
29
|
+
}): Promise<Uint8Array | null>;
|
|
30
|
+
export declare function parseGgufHeader(bytes: Uint8Array): GgufHeader | null;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import b4a from 'b4a';
|
|
2
|
+
import { AbortController } from 'bare-abort-controller';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import defaultFetch from '#fetch';
|
|
8
|
+
export const GGUF_HEADER_BYTES = 1_048_576;
|
|
9
|
+
export const GGUF_HEADER_TIMEOUT_MS = 30_000;
|
|
10
|
+
const UINT8 = 0;
|
|
11
|
+
const INT8 = 1;
|
|
12
|
+
const UINT16 = 2;
|
|
13
|
+
const INT16 = 3;
|
|
14
|
+
const UINT32 = 4;
|
|
15
|
+
const INT32 = 5;
|
|
16
|
+
const FLOAT32 = 6;
|
|
17
|
+
const BOOL = 7;
|
|
18
|
+
const STRING = 8;
|
|
19
|
+
const ARRAY = 9;
|
|
20
|
+
const UINT64 = 10;
|
|
21
|
+
const INT64 = 11;
|
|
22
|
+
const FLOAT64 = 12;
|
|
23
|
+
export function isRemoteSource(src) {
|
|
24
|
+
return /^https?:\/\//i.test(src);
|
|
25
|
+
}
|
|
26
|
+
export function localSourcePath(src) {
|
|
27
|
+
if (/^file:/i.test(src))
|
|
28
|
+
return filePath(src);
|
|
29
|
+
if (/^[a-z][a-z0-9+.-]+:/i.test(src))
|
|
30
|
+
return null;
|
|
31
|
+
if (src === '~')
|
|
32
|
+
return os.homedir();
|
|
33
|
+
if (src.startsWith('~/'))
|
|
34
|
+
return path.join(os.homedir(), src.slice(2));
|
|
35
|
+
return src;
|
|
36
|
+
}
|
|
37
|
+
function filePath(src) {
|
|
38
|
+
try {
|
|
39
|
+
return fileURLToPath(src);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function readGgufHeaderBytes(src, opts = {}) {
|
|
46
|
+
const maxBytes = opts.maxBytes ?? GGUF_HEADER_BYTES;
|
|
47
|
+
return isRemoteSource(src)
|
|
48
|
+
? fetchHead(src, maxBytes, opts.fetch ?? defaultFetch)
|
|
49
|
+
: readLocalHead(src, maxBytes);
|
|
50
|
+
}
|
|
51
|
+
async function fetchHead(src, maxBytes, fetch) {
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timeout = setTimeout(() => controller.abort(undefined), GGUF_HEADER_TIMEOUT_MS);
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(src, {
|
|
56
|
+
headers: { Range: `bytes=0-${maxBytes - 1}` },
|
|
57
|
+
signal: controller.signal
|
|
58
|
+
});
|
|
59
|
+
if (!response.ok)
|
|
60
|
+
return null;
|
|
61
|
+
const length = response.headers.get('content-length');
|
|
62
|
+
if (length === null || !(Number(length) <= maxBytes))
|
|
63
|
+
return null;
|
|
64
|
+
return new Uint8Array(await response.arrayBuffer()).subarray(0, maxBytes);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function readLocalHead(src, maxBytes) {
|
|
74
|
+
const file = localSourcePath(src);
|
|
75
|
+
if (file === null)
|
|
76
|
+
return null;
|
|
77
|
+
let handle;
|
|
78
|
+
try {
|
|
79
|
+
handle = await fs.promises.open(file, 'r');
|
|
80
|
+
const buffer = b4a.allocUnsafe(maxBytes);
|
|
81
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
82
|
+
return buffer.subarray(0, bytesRead);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await handle?.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function parseGgufHeader(bytes) {
|
|
92
|
+
if (bytes.byteLength < 24)
|
|
93
|
+
return null;
|
|
94
|
+
if (b4a.toString(bytes.subarray(0, 4), 'latin1') !== 'GGUF')
|
|
95
|
+
return null;
|
|
96
|
+
const c = {
|
|
97
|
+
view: new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength),
|
|
98
|
+
bytes,
|
|
99
|
+
offset: 16
|
|
100
|
+
};
|
|
101
|
+
if (c.view.getUint32(4, true) < 2)
|
|
102
|
+
return null;
|
|
103
|
+
const count = readU64(c);
|
|
104
|
+
const header = {};
|
|
105
|
+
const contextLengths = new Map();
|
|
106
|
+
for (let i = 0; i < count; i += 1) {
|
|
107
|
+
const key = readString(c);
|
|
108
|
+
if (key === null || !has(c, 4))
|
|
109
|
+
break;
|
|
110
|
+
const value = readValue(c, readU32(c), key === 'general.tags');
|
|
111
|
+
if (!value.ok)
|
|
112
|
+
break;
|
|
113
|
+
if (key === 'general.architecture')
|
|
114
|
+
header.architecture = value.str;
|
|
115
|
+
else if (key === 'general.name')
|
|
116
|
+
header.name = value.str;
|
|
117
|
+
else if (key === 'general.type')
|
|
118
|
+
header.type = value.str;
|
|
119
|
+
else if (key === 'general.license')
|
|
120
|
+
header.license = value.str;
|
|
121
|
+
else if (key === 'general.size_label')
|
|
122
|
+
header.sizeLabel = value.str;
|
|
123
|
+
else if (key === 'general.tags')
|
|
124
|
+
header.tags = value.strs;
|
|
125
|
+
else if (key.endsWith('.context_length'))
|
|
126
|
+
contextLengths.set(key, value.num);
|
|
127
|
+
}
|
|
128
|
+
header.contextLength =
|
|
129
|
+
contextLengths.get(`${header.architecture}.context_length`) ??
|
|
130
|
+
contextLengths.values().next().value;
|
|
131
|
+
return header;
|
|
132
|
+
}
|
|
133
|
+
function readValue(c, type, collect = false) {
|
|
134
|
+
switch (type) {
|
|
135
|
+
case UINT8:
|
|
136
|
+
case BOOL:
|
|
137
|
+
return fixed(c, 1, () => c.view.getUint8(c.offset));
|
|
138
|
+
case INT8:
|
|
139
|
+
return fixed(c, 1, () => c.view.getInt8(c.offset));
|
|
140
|
+
case UINT16:
|
|
141
|
+
return fixed(c, 2, () => c.view.getUint16(c.offset, true));
|
|
142
|
+
case INT16:
|
|
143
|
+
return fixed(c, 2, () => c.view.getInt16(c.offset, true));
|
|
144
|
+
case UINT32:
|
|
145
|
+
return fixed(c, 4, () => c.view.getUint32(c.offset, true));
|
|
146
|
+
case INT32:
|
|
147
|
+
return fixed(c, 4, () => c.view.getInt32(c.offset, true));
|
|
148
|
+
case FLOAT32:
|
|
149
|
+
return fixed(c, 4, () => c.view.getFloat32(c.offset, true));
|
|
150
|
+
case FLOAT64:
|
|
151
|
+
return fixed(c, 8, () => c.view.getFloat64(c.offset, true));
|
|
152
|
+
case UINT64:
|
|
153
|
+
case INT64:
|
|
154
|
+
return has(c, 8) ? { ok: true, num: readU64(c) } : { ok: false };
|
|
155
|
+
case STRING: {
|
|
156
|
+
const str = readString(c);
|
|
157
|
+
return str === null ? { ok: false } : { ok: true, str };
|
|
158
|
+
}
|
|
159
|
+
case ARRAY: {
|
|
160
|
+
if (!has(c, 12))
|
|
161
|
+
return { ok: false };
|
|
162
|
+
const elementType = readU32(c);
|
|
163
|
+
const length = readU64(c);
|
|
164
|
+
const strs = new Array();
|
|
165
|
+
for (let i = 0; i < length; i += 1) {
|
|
166
|
+
const element = readValue(c, elementType);
|
|
167
|
+
if (!element.ok)
|
|
168
|
+
return { ok: false };
|
|
169
|
+
if (collect && element.str !== undefined)
|
|
170
|
+
strs.push(element.str);
|
|
171
|
+
}
|
|
172
|
+
return collect ? { ok: true, strs } : { ok: true };
|
|
173
|
+
}
|
|
174
|
+
default:
|
|
175
|
+
return { ok: false };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function fixed(c, width, read) {
|
|
179
|
+
if (!has(c, width))
|
|
180
|
+
return { ok: false };
|
|
181
|
+
const num = read();
|
|
182
|
+
c.offset += width;
|
|
183
|
+
return { ok: true, num };
|
|
184
|
+
}
|
|
185
|
+
function has(c, n) {
|
|
186
|
+
return n >= 0 && c.offset + n <= c.bytes.byteLength;
|
|
187
|
+
}
|
|
188
|
+
function readU32(c) {
|
|
189
|
+
const value = c.view.getUint32(c.offset, true);
|
|
190
|
+
c.offset += 4;
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
function readU64(c) {
|
|
194
|
+
const lo = c.view.getUint32(c.offset, true);
|
|
195
|
+
const hi = c.view.getUint32(c.offset + 4, true);
|
|
196
|
+
c.offset += 8;
|
|
197
|
+
return hi * 2 ** 32 + lo;
|
|
198
|
+
}
|
|
199
|
+
function readString(c) {
|
|
200
|
+
if (!has(c, 8))
|
|
201
|
+
return null;
|
|
202
|
+
const length = readU64(c);
|
|
203
|
+
if (!has(c, length))
|
|
204
|
+
return null;
|
|
205
|
+
const str = b4a.toString(c.bytes.subarray(c.offset, c.offset + length), 'utf8');
|
|
206
|
+
c.offset += length;
|
|
207
|
+
return str;
|
|
208
|
+
}
|
|
@@ -105,13 +105,18 @@ export interface CuratedListable {
|
|
|
105
105
|
}
|
|
106
106
|
export declare function curatedListables(): CuratedListable[];
|
|
107
107
|
export declare function curatedModelAsset(name: string, addon: ModelRegistryEntry['addon']): SdkModelAsset | undefined;
|
|
108
|
+
export declare const CUSTOM_TASK_TYPES: readonly ['llm'];
|
|
109
|
+
export type CustomTaskType = (typeof CUSTOM_TASK_TYPES)[number];
|
|
108
110
|
export interface CustomModelEntry {
|
|
109
111
|
readonly name: string;
|
|
110
112
|
readonly src: string;
|
|
111
113
|
readonly projectionSrc?: string;
|
|
114
|
+
readonly taskType?: CustomTaskType;
|
|
115
|
+
readonly expectedSize?: number;
|
|
112
116
|
}
|
|
113
117
|
export declare function setCustomModels(entries: ReadonlyArray<CustomModelEntry>): void;
|
|
114
|
-
export declare function
|
|
118
|
+
export declare function customModels(): ReadonlyArray<CustomModelEntry>;
|
|
119
|
+
export declare function customModelSrc(name: string, addon: ModelRegistryEntry['addon']): string | undefined;
|
|
115
120
|
export declare function customToolDialect(name: string): ToolDialect | undefined;
|
|
116
121
|
export declare function customProjectionMap(): Record<string, string>;
|
|
117
122
|
export declare function companionRegistryPaths(): Map<string, string[]>;
|
|
@@ -311,12 +311,17 @@ export function curatedListables() {
|
|
|
311
311
|
export function curatedModelAsset(name, addon) {
|
|
312
312
|
return curatedListables().find((entry) => entry.asset.addon === addon && (entry.name === name || entry.asset.name === name))?.asset;
|
|
313
313
|
}
|
|
314
|
+
export const CUSTOM_TASK_TYPES = ['llm'];
|
|
314
315
|
let CUSTOM_MODELS = new Map();
|
|
315
316
|
export function setCustomModels(entries) {
|
|
316
317
|
CUSTOM_MODELS = new Map(entries.map((entry) => [entry.name, entry]));
|
|
317
318
|
}
|
|
318
|
-
export function
|
|
319
|
-
return CUSTOM_MODELS.
|
|
319
|
+
export function customModels() {
|
|
320
|
+
return [...CUSTOM_MODELS.values()];
|
|
321
|
+
}
|
|
322
|
+
export function customModelSrc(name, addon) {
|
|
323
|
+
const entry = CUSTOM_MODELS.get(name);
|
|
324
|
+
return entry && (entry.taskType ?? 'llm') === addon ? entry.src : undefined;
|
|
320
325
|
}
|
|
321
326
|
export function customToolDialect(name) {
|
|
322
327
|
const entry = CUSTOM_MODELS.get(name);
|
|
@@ -15,4 +15,7 @@ export interface DeleteModelSummary {
|
|
|
15
15
|
readonly freedBytes: number;
|
|
16
16
|
}
|
|
17
17
|
export declare function summarizeDelete(result: DeleteModelResult): DeleteModelSummary;
|
|
18
|
-
export
|
|
18
|
+
export interface DeleteModelOptions {
|
|
19
|
+
readonly modelsDir?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function deleteModelFiles(sdk: ModelDeleteSdk, name: string, { modelsDir }?: DeleteModelOptions): Promise<DeleteModelResult>;
|
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import {
|
|
2
|
+
import { downloadedCustomFiles } from './custom-model-cache.js';
|
|
3
|
+
import { curatedListables, customModels, musicListables } from './model-catalog.js';
|
|
3
4
|
export function summarizeDelete(result) {
|
|
4
5
|
return { name: result.name, removed: result.removed.length, freedBytes: result.freedBytes };
|
|
5
6
|
}
|
|
6
|
-
export async function deleteModelFiles(sdk, name) {
|
|
7
|
-
const
|
|
7
|
+
export async function deleteModelFiles(sdk, name, { modelsDir } = {}) {
|
|
8
|
+
const custom = customModels().find((entry) => entry.name === name);
|
|
9
|
+
const paths = custom ? downloadedCustomFiles(custom, modelsDir) : await catalogFiles(sdk, name);
|
|
8
10
|
const removed = [];
|
|
11
|
+
for (const filePath of new Set(paths)) {
|
|
12
|
+
const bytes = await onDiskSize(filePath);
|
|
13
|
+
if (bytes === null)
|
|
14
|
+
continue;
|
|
15
|
+
await fs.promises.rm(filePath, { force: true });
|
|
16
|
+
removed.push({ path: filePath, bytes });
|
|
17
|
+
}
|
|
18
|
+
return { name, removed, freedBytes: removed.reduce((sum, file) => sum + file.bytes, 0) };
|
|
19
|
+
}
|
|
20
|
+
async function catalogFiles(sdk, name) {
|
|
21
|
+
const paths = [];
|
|
9
22
|
for (const catalogName of namesToPurge(name)) {
|
|
10
23
|
const info = await sdk.getModelInfo({ name: catalogName }).catch(() => null);
|
|
11
24
|
if (!info)
|
|
12
25
|
continue;
|
|
13
|
-
for (const file of info.cacheFiles)
|
|
14
|
-
|
|
15
|
-
continue;
|
|
16
|
-
seen.add(file.path);
|
|
17
|
-
const bytes = await onDiskSize(file.path);
|
|
18
|
-
if (bytes === null)
|
|
19
|
-
continue;
|
|
20
|
-
await fs.promises.rm(file.path, { force: true });
|
|
21
|
-
removed.push({ path: file.path, bytes });
|
|
22
|
-
}
|
|
26
|
+
for (const file of info.cacheFiles)
|
|
27
|
+
paths.push(file.path);
|
|
23
28
|
}
|
|
24
|
-
return
|
|
29
|
+
return paths;
|
|
25
30
|
}
|
|
26
31
|
async function onDiskSize(filePath) {
|
|
27
32
|
try {
|
|
@@ -39,4 +39,10 @@ export declare const MODEL_STATUS_TTL_MS = 30000;
|
|
|
39
39
|
export declare function createModelStatusCache(): ModelStatusCache;
|
|
40
40
|
export declare function invalidateModelStatusCache(cache: ModelStatusCache): void;
|
|
41
41
|
export declare function listModels(sdk: ModelListSdk, { type, cachedOnly, fullRegistry }?: ListModelsOptions, cache?: ModelStatusCache, projectionFor?: (name: string) => string | undefined): Promise<ModelInfo[]>;
|
|
42
|
+
export interface CustomModelRowsContext {
|
|
43
|
+
readonly modelsDir?: string;
|
|
44
|
+
readonly loaded: ReadonlySet<string>;
|
|
45
|
+
}
|
|
46
|
+
export declare function customModelRows({ type, cachedOnly }: ListModelsOptions, { modelsDir, loaded }: CustomModelRowsContext): Promise<ModelInfo[]>;
|
|
47
|
+
export declare function mergeCustomRows(catalog: ReadonlyArray<ModelInfo>, custom: ReadonlyArray<ModelInfo>): ModelInfo[];
|
|
42
48
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isCustomModelCached } from './custom-model-cache.js';
|
|
2
|
+
import { companionRegistryPaths, curatedListables, customModels, musicListables } from './model-catalog.js';
|
|
2
3
|
export const MODEL_STATUS_TTL_MS = 30_000;
|
|
3
4
|
export function createModelStatusCache() {
|
|
4
5
|
return { entries: new Map(), epoch: 0 };
|
|
@@ -121,3 +122,24 @@ export async function listModels(sdk, { type, cachedOnly, fullRegistry } = {}, c
|
|
|
121
122
|
});
|
|
122
123
|
return cachedOnly ? rows.filter((row) => row.isCached) : rows;
|
|
123
124
|
}
|
|
125
|
+
export async function customModelRows({ type, cachedOnly }, { modelsDir, loaded }) {
|
|
126
|
+
const entries = customModels().filter((entry) => !type || (entry.taskType ?? 'llm') === type);
|
|
127
|
+
const rows = await Promise.all(entries.map(async (entry) => {
|
|
128
|
+
const addon = entry.taskType ?? 'llm';
|
|
129
|
+
return {
|
|
130
|
+
name: entry.name,
|
|
131
|
+
type: addon,
|
|
132
|
+
registryPath: entry.src,
|
|
133
|
+
registrySource: 'custom',
|
|
134
|
+
isCached: await isCustomModelCached(entry, modelsDir),
|
|
135
|
+
isLoaded: loaded.has(entry.name),
|
|
136
|
+
supportsImages: addon === 'llm' && Boolean(entry.projectionSrc),
|
|
137
|
+
expectedSize: entry.expectedSize ?? 0
|
|
138
|
+
};
|
|
139
|
+
}));
|
|
140
|
+
return cachedOnly ? rows.filter((row) => row.isCached) : rows;
|
|
141
|
+
}
|
|
142
|
+
export function mergeCustomRows(catalog, custom) {
|
|
143
|
+
const shadowed = new Set(custom.map((row) => row.name));
|
|
144
|
+
return [...catalog.filter((row) => !shadowed.has(row.name)), ...custom];
|
|
145
|
+
}
|
|
@@ -2,7 +2,7 @@ import { audioGenModelByName, curatedModelAsset, customModelSrc } from './model-
|
|
|
2
2
|
export async function resolveModelSrc(sdk, model, addon = 'llm') {
|
|
3
3
|
if (/^(\/|~|\.|file:|https?:|registry:|pear:|hyper:)/.test(model))
|
|
4
4
|
return model;
|
|
5
|
-
const custom = customModelSrc(model);
|
|
5
|
+
const custom = customModelSrc(model, addon);
|
|
6
6
|
if (custom)
|
|
7
7
|
return custom;
|
|
8
8
|
if (addon === 'audiogen') {
|
package/dist-lib/lib/serve.js
CHANGED
|
@@ -5,6 +5,7 @@ import { pipeline, Readable } from 'streamx';
|
|
|
5
5
|
import { bindApi } from '../spec/bind.js';
|
|
6
6
|
import HRPC from '../spec/hrpc/index.js';
|
|
7
7
|
import { isCapability } from './capabilities/invoke.js';
|
|
8
|
+
import { CUSTOM_TASK_TYPES } from './models/model-catalog.js';
|
|
8
9
|
import { unsupportedKvCacheDeletion } from './capabilities/kv-cache.js';
|
|
9
10
|
import { errorMessage } from './error-fields.js';
|
|
10
11
|
import { hlog } from './log.js';
|
|
@@ -65,13 +66,28 @@ export function serveHarness(stream, harness) {
|
|
|
65
66
|
removed: (await harness.deleteOverlaySkill?.(name))?.removed ?? false
|
|
66
67
|
}),
|
|
67
68
|
setCustomModels: async ({ models }) => {
|
|
68
|
-
await harness.setCustomModels?.((models ?? []).
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
await harness.setCustomModels?.((models ?? []).flatMap(({ name, src, projectionSrc, taskType, expectedSize }) => {
|
|
70
|
+
const known = CUSTOM_TASK_TYPES.find((task) => task === taskType);
|
|
71
|
+
if (taskType && !known)
|
|
72
|
+
return [];
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
name,
|
|
76
|
+
src,
|
|
77
|
+
projectionSrc: projectionSrc ?? undefined,
|
|
78
|
+
taskType: known,
|
|
79
|
+
expectedSize: expectedSize || undefined
|
|
80
|
+
}
|
|
81
|
+
];
|
|
82
|
+
}));
|
|
73
83
|
return { ok: true };
|
|
74
84
|
},
|
|
85
|
+
previewModelSource: async ({ src, mmprojSrc }) => {
|
|
86
|
+
if (!harness.previewModelSource) {
|
|
87
|
+
throw new Error('harness does not support previewModelSource');
|
|
88
|
+
}
|
|
89
|
+
return harness.previewModelSource({ src: src ?? '', mmprojSrc: mmprojSrc ?? undefined });
|
|
90
|
+
},
|
|
75
91
|
describe: async () => (await harness.describe?.()) ?? {
|
|
76
92
|
toolNames: [],
|
|
77
93
|
skills: [],
|
|
@@ -564,7 +564,9 @@ export function registerHarnessTypes(ns) {
|
|
|
564
564
|
fields: [
|
|
565
565
|
{ name: 'name', type: 'string', required: true },
|
|
566
566
|
{ name: 'src', type: 'string', required: true },
|
|
567
|
-
{ name: 'projectionSrc', type: 'string' }
|
|
567
|
+
{ name: 'projectionSrc', type: 'string' },
|
|
568
|
+
{ name: 'taskType', type: 'string' },
|
|
569
|
+
{ name: 'expectedSize', type: 'uint' }
|
|
568
570
|
]
|
|
569
571
|
});
|
|
570
572
|
ns.register({
|
|
@@ -678,6 +680,40 @@ export function registerHarnessTypes(ns) {
|
|
|
678
680
|
{ name: 'verify', type: '@harness/skill-oauth-verify' }
|
|
679
681
|
]
|
|
680
682
|
});
|
|
683
|
+
ns.register({
|
|
684
|
+
name: 'preview-model-source-request',
|
|
685
|
+
fields: [
|
|
686
|
+
{ name: 'src', type: 'string', required: true },
|
|
687
|
+
{ name: 'mmprojSrc', type: 'string' }
|
|
688
|
+
]
|
|
689
|
+
});
|
|
690
|
+
ns.register({
|
|
691
|
+
name: 'model-source-file',
|
|
692
|
+
fields: [
|
|
693
|
+
{ name: 'src', type: 'string', required: true },
|
|
694
|
+
{ name: 'label', type: 'string', required: true },
|
|
695
|
+
{ name: 'sizeBytes', type: 'uint' }
|
|
696
|
+
]
|
|
697
|
+
});
|
|
698
|
+
ns.register({
|
|
699
|
+
name: 'preview-model-source-response',
|
|
700
|
+
fields: [
|
|
701
|
+
{ name: 'ok', type: 'bool', required: true },
|
|
702
|
+
{ name: 'refusal', type: 'string' },
|
|
703
|
+
{ name: 'reason', type: 'string' },
|
|
704
|
+
{ name: 'src', type: 'string' },
|
|
705
|
+
{ name: 'files', type: '@harness/model-source-file', array: true },
|
|
706
|
+
{ name: 'sizeBytes', type: 'uint' },
|
|
707
|
+
{ name: 'contextLength', type: 'uint' },
|
|
708
|
+
{ name: 'architecture', type: 'string' },
|
|
709
|
+
{ name: 'sizeLabel', type: 'string' },
|
|
710
|
+
{ name: 'taskType', type: 'string' },
|
|
711
|
+
{ name: 'license', type: 'string' },
|
|
712
|
+
{ name: 'fit', type: 'string' },
|
|
713
|
+
{ name: 'fitReasons', type: 'string', array: true },
|
|
714
|
+
{ name: 'mmprojSrc', type: 'string' }
|
|
715
|
+
]
|
|
716
|
+
});
|
|
681
717
|
}
|
|
682
718
|
export function registerHarnessApi(api) {
|
|
683
719
|
api.register({
|
|
@@ -825,4 +861,9 @@ export function registerHarnessApi(api) {
|
|
|
825
861
|
request: { name: '@harness/detect-language-request', stream: false },
|
|
826
862
|
response: { name: '@harness/detect-language-response', stream: false }
|
|
827
863
|
});
|
|
864
|
+
api.register({
|
|
865
|
+
name: 'preview-model-source',
|
|
866
|
+
request: { name: '@harness/preview-model-source-request', stream: false },
|
|
867
|
+
response: { name: '@harness/preview-model-source-response', stream: false }
|
|
868
|
+
});
|
|
828
869
|
}
|
package/dist-lib/spec/bind.js
CHANGED
package/dist-lib/spec/calls.js
CHANGED
|
@@ -39,5 +39,6 @@ export function harnessClient(rpc) {
|
|
|
39
39
|
deleteOverlaySkill: (input) => rpc.deleteOverlaySkill(input),
|
|
40
40
|
deleteKvCache: (input) => rpc.deleteKvCache(input),
|
|
41
41
|
detectLanguage: (input) => rpc.detectLanguage(input),
|
|
42
|
+
previewModelSource: (input) => rpc.previewModelSource(input),
|
|
42
43
|
};
|
|
43
44
|
}
|
|
@@ -377,6 +377,19 @@
|
|
|
377
377
|
"stream": false
|
|
378
378
|
},
|
|
379
379
|
"version": 1
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
"id": 29,
|
|
383
|
+
"name": "@harness/preview-model-source",
|
|
384
|
+
"request": {
|
|
385
|
+
"name": "@harness/preview-model-source-request",
|
|
386
|
+
"stream": false
|
|
387
|
+
},
|
|
388
|
+
"response": {
|
|
389
|
+
"name": "@harness/preview-model-source-response",
|
|
390
|
+
"stream": false
|
|
391
|
+
},
|
|
392
|
+
"version": 1
|
|
380
393
|
}
|
|
381
394
|
]
|
|
382
395
|
}
|
|
@@ -63,7 +63,9 @@ const methods = new Map([
|
|
|
63
63
|
['@harness/delete-kv-cache', 27],
|
|
64
64
|
[27, '@harness/delete-kv-cache'],
|
|
65
65
|
['@harness/detect-language', 28],
|
|
66
|
-
[28, '@harness/detect-language']
|
|
66
|
+
[28, '@harness/detect-language'],
|
|
67
|
+
['@harness/preview-model-source', 29],
|
|
68
|
+
[29, '@harness/preview-model-source']
|
|
67
69
|
])
|
|
68
70
|
|
|
69
71
|
class HRPC {
|
|
@@ -99,7 +101,8 @@ class HRPC {
|
|
|
99
101
|
['@harness/copy-skill-for-edit', getEncoding('@harness/skill-name-request')],
|
|
100
102
|
['@harness/delete-overlay-skill', getEncoding('@harness/skill-name-request')],
|
|
101
103
|
['@harness/delete-kv-cache', getEncoding('@harness/kv-cache-delete-request')],
|
|
102
|
-
['@harness/detect-language', getEncoding('@harness/detect-language-request')]
|
|
104
|
+
['@harness/detect-language', getEncoding('@harness/detect-language-request')],
|
|
105
|
+
['@harness/preview-model-source', getEncoding('@harness/preview-model-source-request')]
|
|
103
106
|
])
|
|
104
107
|
this._responseEncodings = new Map([
|
|
105
108
|
['@harness/completion', getEncoding('@harness/completion-event')],
|
|
@@ -130,7 +133,8 @@ class HRPC {
|
|
|
130
133
|
['@harness/copy-skill-for-edit', getEncoding('@harness/copy-skill-response')],
|
|
131
134
|
['@harness/delete-overlay-skill', getEncoding('@harness/delete-skill-response')],
|
|
132
135
|
['@harness/delete-kv-cache', getEncoding('@harness/kv-cache-deletion-response')],
|
|
133
|
-
['@harness/detect-language', getEncoding('@harness/detect-language-response')]
|
|
136
|
+
['@harness/detect-language', getEncoding('@harness/detect-language-response')],
|
|
137
|
+
['@harness/preview-model-source', getEncoding('@harness/preview-model-source-response')]
|
|
134
138
|
])
|
|
135
139
|
this._rpc = new RPC(stream, async (req) => {
|
|
136
140
|
const command = methods.get(req.command)
|
|
@@ -344,6 +348,10 @@ class HRPC {
|
|
|
344
348
|
return this._call('@harness/detect-language', args)
|
|
345
349
|
}
|
|
346
350
|
|
|
351
|
+
async previewModelSource(args) {
|
|
352
|
+
return this._call('@harness/preview-model-source', args)
|
|
353
|
+
}
|
|
354
|
+
|
|
347
355
|
onCompletion(responseFn) {
|
|
348
356
|
this._handlers['@harness/completion'] = responseFn
|
|
349
357
|
}
|
|
@@ -460,6 +468,10 @@ class HRPC {
|
|
|
460
468
|
this._handlers['@harness/detect-language'] = responseFn
|
|
461
469
|
}
|
|
462
470
|
|
|
471
|
+
onPreviewModelSource(responseFn) {
|
|
472
|
+
this._handlers['@harness/preview-model-source'] = responseFn
|
|
473
|
+
}
|
|
474
|
+
|
|
463
475
|
_requestIsStream(command) {
|
|
464
476
|
return [
|
|
465
477
|
'@harness/completion',
|