@qvac/harness 0.5.0 → 0.7.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 +142 -1
- package/README.md +78 -27
- package/dist-lib/index.d.ts +15 -1
- package/dist-lib/index.js +175 -53
- package/dist-lib/lib/capabilities/preview-model-source.d.ts +0 -1
- package/dist-lib/lib/connect.d.ts +3 -1
- package/dist-lib/lib/connect.js +23 -2
- package/dist-lib/lib/contract-types.d.ts +14 -0
- package/dist-lib/lib/docx/index.d.ts +1 -1
- package/dist-lib/lib/docx/index.js +1 -1
- package/dist-lib/lib/hardware/capability-probe.js +9 -4
- package/dist-lib/lib/hooks/built-in/sandbox.js +0 -5
- package/dist-lib/lib/models/model-catalog-constants.d.ts +7 -0
- package/dist-lib/lib/models/model-catalog-constants.js +17 -0
- package/dist-lib/lib/models/model-catalog.d.ts +16 -6
- package/dist-lib/lib/models/model-catalog.js +21 -6
- package/dist-lib/lib/models/model-list.d.ts +2 -0
- package/dist-lib/lib/models/model-list.js +9 -4
- package/dist-lib/lib/models/model-pool.d.ts +6 -2
- package/dist-lib/lib/models/model-pool.js +67 -11
- package/dist-lib/lib/models/model-task.d.ts +3 -0
- package/dist-lib/lib/models/model-task.js +14 -0
- package/dist-lib/lib/models/music-catalog.d.ts +2 -0
- package/dist-lib/lib/models/music-catalog.js +9 -0
- package/dist-lib/lib/office/index.d.ts +1 -1
- package/dist-lib/lib/office/index.js +1 -1
- package/dist-lib/lib/office/python.js +1 -2
- package/dist-lib/lib/serve.js +12 -0
- package/dist-lib/lib/tools/exec/adapters/python/deadline-state.d.ts +10 -0
- package/dist-lib/lib/tools/exec/adapters/python/deadline-state.js +5 -0
- package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.js +10 -17
- package/dist-lib/lib/tools/exec/adapters/python/loader.js +15 -11
- package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +1 -15
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +1 -0
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +13 -2
- package/dist-lib/lib/tools/exec/adapters/python/wheels.d.ts +3 -3
- package/dist-lib/lib/tools/exec/adapters/python/wheels.js +3 -14
- package/dist-lib/lib/tools/exec/adapters/python/worker/active.d.ts +19 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/active.js +72 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/client.d.ts +0 -1
- package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +8 -76
- package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +2 -126
- package/dist-lib/lib/tools/exec/adapters/python/worker/interrupt-deadline.d.ts +1 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/interrupt-deadline.js +21 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/serve.d.ts +2 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/serve.js +90 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/{spawn-node.d.ts → spawn.d.ts} +0 -1
- package/dist-lib/lib/tools/exec/adapters/python/worker/{spawn-node.js → spawn.js} +0 -8
- package/dist-lib/lib/tools/exec/bare-runtime.d.ts +2 -0
- package/dist-lib/lib/tools/exec/bare-runtime.js +10 -0
- package/dist-lib/lib/tools/exec.d.ts +0 -2
- package/dist-lib/lib/tools/exec.js +6 -11
- package/dist-lib/schema/harness.js +27 -1
- package/dist-lib/spec/bind.js +6 -0
- package/dist-lib/spec/calls.js +2 -0
- package/dist-lib/spec/hrpc/hrpc.json +26 -0
- package/dist-lib/spec/hrpc/index.js +27 -3
- package/dist-lib/spec/hrpc/messages.js +72 -5
- package/dist-lib/spec/hyperschema/index.js +72 -5
- package/dist-lib/spec/hyperschema/schema.json +53 -1
- package/dist-lib/spec/hyperschema/types.d.ts +18 -1
- package/package.json +40 -20
- package/dist/harness.bundle +0 -42312
- package/dist/harness.cjs +0 -18
- package/dist/harness.d.ts +0 -8
- package/dist/prebuilds-manifest.json +0 -372
- package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.d.ts +0 -3
- package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.js +0 -38
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AUDIOGEN_ACESTEP_5HZ_LM_0_6B_Q8_0, AUDIOGEN_ACESTEP_V15_SFT_Q8_0, AUDIOGEN_ACESTEP_V15_TURBO_Q4_K_M, AUDIOGEN_ACESTEP_V15_TURBO_Q8_0, AUDIOGEN_QWEN3_EMBEDDING_0_6B_Q8_0, AUDIOGEN_VAE_BF16, EMBEDDINGGEMMA_300M_Q4_0, FLUX_2_KLEIN_4B_Q4_0, FLUX_2_KLEIN_4B_VAE, HEALTHCARE_1_7B_MEDICAL_Q4_K_M, HEALTHCARE_4B_MEDICAL_Q4_K_M, MMPROJ_QWEN3_5_2B_MULTIMODAL_Q8_0, MMPROJ_QWEN3_5_4B_MULTIMODAL_Q8_0, MMPROJ_QWEN3_5_9B_MULTIMODAL_F16, MMPROJ_QWEN3_6_27B_MULTIMODAL_F16, MMPROJ_QWEN3_6_35B_A3B_MULTIMODAL_F16, MMPROJ_QWEN3_8_27B_MULTIMODAL_F16, QWEN3_5_2B_MULTIMODAL_Q4_K_M, QWEN3_5_4B_MULTIMODAL_Q4_K_M, QWEN3_5_9B_MULTIMODAL_Q4_K_M, PARAKEET_TDT_0_6B_V3_Q8_0, QWEN3_4B_Q4_K_M, QWEN3_6_27B_MULTIMODAL_Q4_K_XL, QWEN3_6_35B_A3B_MULTIMODAL_Q4_K_M, QWEN3_8_27B_MULTIMODAL_UD_Q4_K_XL, SD_V2_1_1B_Q8_0, SDXL_BASE_1_0_3B_Q4_0, TTS_MULTILINGUAL_SUPERTONIC3_Q4_0, TTS_MULTILINGUAL_SUPERTONIC3_Q8_0 } from '@qvac/sdk/models';
|
|
2
2
|
import { detectToolDialect } from './tool-dialect.js';
|
|
3
3
|
import { defaultTtsModelName } from './tts-model-default.js';
|
|
4
|
-
import { CAPABILITY_PROFILE_ORDER, QWEN_35_2B, QWEN_35_4B, QWEN_35_9B, QWEN_36_27B, QWEN_36_35B_A3B, QWEN_38_27B, MEDPSY_1_7B, MEDPSY_4B, isCapabilityProfile, meetsMinCapabilityProfile, modelNameToCapabilityProfile, capabilityProfileToModelName, isMultimodalLlm, AGENT_MODEL_NAMES, modelDisplayName, capabilityProfileModelLabel, DEFAULT_IMAGE_MODEL_CHOICE, IMAGE_CHOICE_CATALOG } from './model-catalog-constants.js';
|
|
5
|
-
export { CAPABILITY_PROFILE_ORDER, QWEN_35_2B, QWEN_35_4B, QWEN_35_9B, QWEN_36_27B, QWEN_36_35B_A3B, QWEN_38_27B, MEDPSY_1_7B, MEDPSY_4B, isCapabilityProfile, meetsMinCapabilityProfile, modelNameToCapabilityProfile, capabilityProfileToModelName, isMultimodalLlm, AGENT_MODEL_NAMES, modelDisplayName, capabilityProfileModelLabel, DEFAULT_IMAGE_MODEL_CHOICE, IMAGE_CHOICE_CATALOG };
|
|
4
|
+
import { CAPABILITY_PROFILE_ORDER, QWEN_35_2B, QWEN_35_4B, QWEN_35_9B, QWEN_36_27B, QWEN_36_35B_A3B, QWEN_38_27B, MEDPSY_1_7B, MEDPSY_4B, isCapabilityProfile, meetsMinCapabilityProfile, modelNameToCapabilityProfile, capabilityProfileToModelName, isMultimodalLlm, AGENT_MODEL_NAMES, modelDisplayName, capabilityProfileModelLabel, DEFAULT_IMAGE_MODEL_CHOICE, IMAGE_CHOICE_CATALOG, isMobilePlatform, runnableModelName } from './model-catalog-constants.js';
|
|
5
|
+
export { CAPABILITY_PROFILE_ORDER, QWEN_35_2B, QWEN_35_4B, QWEN_35_9B, QWEN_36_27B, QWEN_36_35B_A3B, QWEN_38_27B, MEDPSY_1_7B, MEDPSY_4B, isCapabilityProfile, meetsMinCapabilityProfile, modelNameToCapabilityProfile, capabilityProfileToModelName, isMultimodalLlm, AGENT_MODEL_NAMES, modelDisplayName, capabilityProfileModelLabel, DEFAULT_IMAGE_MODEL_CHOICE, IMAGE_CHOICE_CATALOG, isMobilePlatform, runnableModelName };
|
|
6
6
|
export const LLM_CATALOG = [
|
|
7
7
|
{
|
|
8
8
|
name: QWEN_35_2B,
|
|
@@ -165,8 +165,8 @@ export function diffusionDownloadMetadata(model, addon) {
|
|
|
165
165
|
assetSizes
|
|
166
166
|
};
|
|
167
167
|
}
|
|
168
|
-
import { DEFAULT_MUSIC_MODEL_CHOICE } from './music-catalog.js';
|
|
169
|
-
export { DEFAULT_MUSIC_MODEL_CHOICE };
|
|
168
|
+
import { DEFAULT_MUSIC_MODEL_CHOICE, isMusicModelChoice, musicModelChoiceFrom } from './music-catalog.js';
|
|
169
|
+
export { DEFAULT_MUSIC_MODEL_CHOICE, isMusicModelChoice, musicModelChoiceFrom };
|
|
170
170
|
const MUSIC_GENERATION_SKILL = 'music-generation';
|
|
171
171
|
export const AUDIOGEN_CATALOG = [
|
|
172
172
|
{
|
|
@@ -251,18 +251,26 @@ export function skillModelsForAgent(agent) {
|
|
|
251
251
|
const choice = agent.imageModelChoice ?? DEFAULT_IMAGE_MODEL_CHOICE;
|
|
252
252
|
const entry = DIFFUSION_CATALOG.find((candidate) => candidate.choice === choice);
|
|
253
253
|
if (entry) {
|
|
254
|
-
models.push({ name: entry.name, addon: entry.model.addon });
|
|
254
|
+
models.push({ name: entry.name, addon: entry.model.addon, use: 'image' });
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
if (agentHasSkill(agent, MUSIC_GENERATION_SKILL)) {
|
|
258
258
|
const choice = agent.musicModelChoice ?? DEFAULT_MUSIC_MODEL_CHOICE;
|
|
259
259
|
const entry = AUDIOGEN_CATALOG.find((candidate) => candidate.choice === choice);
|
|
260
260
|
if (entry) {
|
|
261
|
-
models.push({ name: entry.name, addon: entry.dit.addon });
|
|
261
|
+
models.push({ name: entry.name, addon: entry.dit.addon, use: 'music' });
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
return models;
|
|
265
265
|
}
|
|
266
|
+
export function modelsForAgent(agent, host) {
|
|
267
|
+
const skills = skillModelsForAgent(agent);
|
|
268
|
+
if (!agent.modelName) {
|
|
269
|
+
return skills;
|
|
270
|
+
}
|
|
271
|
+
const name = runnableModelName(agent.modelName, host);
|
|
272
|
+
return [{ name, addon: 'llm', use: 'chat' }, ...skills];
|
|
273
|
+
}
|
|
266
274
|
export function diffusionSizeBytes(entry) {
|
|
267
275
|
return diffusionAssets(entry).reduce((sum, asset) => sum + asset.expectedSize, 0);
|
|
268
276
|
}
|
|
@@ -332,6 +340,13 @@ export function curatedListables() {
|
|
|
332
340
|
export function curatedModelAsset(name, addon) {
|
|
333
341
|
return curatedListables().find((entry) => entry.asset.addon === addon && (entry.name === name || entry.asset.name === name))?.asset;
|
|
334
342
|
}
|
|
343
|
+
export function modelAddonOf(model) {
|
|
344
|
+
const known = [...curatedListables(), ...musicListables()].find((entry) => entry.name === model || entry.asset.name === model || entry.asset.src === model);
|
|
345
|
+
return known?.asset.addon ?? 'llm';
|
|
346
|
+
}
|
|
347
|
+
export function residentSrc(model, addon) {
|
|
348
|
+
return curatedModelAsset(model, addon)?.src ?? model;
|
|
349
|
+
}
|
|
335
350
|
export const CUSTOM_TASK_TYPES = ['llm'];
|
|
336
351
|
let CUSTOM_MODELS = new Map();
|
|
337
352
|
export function setCustomModels(entries) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type * as QvacSdk from '@qvac/sdk';
|
|
2
2
|
import type { ModelAddon, ModelRegistrySdk } from './model-source.ts';
|
|
3
|
+
import { type ModelTaskType } from './model-task.ts';
|
|
3
4
|
export type ModelType = ModelAddon;
|
|
4
5
|
export interface StaticCatalogEntry {
|
|
5
6
|
readonly name: string;
|
|
@@ -19,6 +20,7 @@ export interface ModelInfo {
|
|
|
19
20
|
expectedSize: number;
|
|
20
21
|
actualSize?: number;
|
|
21
22
|
label?: string;
|
|
23
|
+
taskType: ModelTaskType;
|
|
22
24
|
}
|
|
23
25
|
export interface ListModelsOptions {
|
|
24
26
|
type?: ModelType;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { addonTaskType } from './model-task.js';
|
|
1
2
|
import { isCustomModelCached } from './custom-model-cache.js';
|
|
2
3
|
import { companionRegistryPaths, curatedListables, customModels, musicListables } from './model-catalog.js';
|
|
3
4
|
export const MODEL_STATUS_TTL_MS = 30_000;
|
|
@@ -42,7 +43,8 @@ async function musicRows(hydrate) {
|
|
|
42
43
|
isCached: status.isCached && companionsCached,
|
|
43
44
|
isLoaded: status.isLoaded,
|
|
44
45
|
supportsImages: false,
|
|
45
|
-
expectedSize: listable.expectedSize
|
|
46
|
+
expectedSize: listable.expectedSize,
|
|
47
|
+
taskType: addonTaskType('audiogen')
|
|
46
48
|
});
|
|
47
49
|
}
|
|
48
50
|
return rows;
|
|
@@ -93,7 +95,8 @@ export async function listModels(sdk, { type, cachedOnly, fullRegistry } = {}, {
|
|
|
93
95
|
isLoaded: status.isLoaded,
|
|
94
96
|
supportsImages: seesImages(listable.name, listable.asset.addon),
|
|
95
97
|
expectedSize: status.expectedSize ?? listable.asset.expectedSize,
|
|
96
|
-
actualSize: status.actualSize
|
|
98
|
+
actualSize: status.actualSize,
|
|
99
|
+
taskType: addonTaskType(listable.asset.addon)
|
|
97
100
|
};
|
|
98
101
|
});
|
|
99
102
|
const music = !type || type === 'audiogen' ? await musicRows(hydrate) : [];
|
|
@@ -121,7 +124,8 @@ export async function listModels(sdk, { type, cachedOnly, fullRegistry } = {}, {
|
|
|
121
124
|
isLoaded: status.isLoaded,
|
|
122
125
|
supportsImages: seesImages(entry.name, entry.addon),
|
|
123
126
|
expectedSize: status.expectedSize ?? entry.expectedSize,
|
|
124
|
-
actualSize: status.actualSize
|
|
127
|
+
actualSize: status.actualSize,
|
|
128
|
+
taskType: addonTaskType(entry.addon)
|
|
125
129
|
};
|
|
126
130
|
});
|
|
127
131
|
return cachedOnly ? rows.filter((row) => row.isCached) : rows;
|
|
@@ -139,7 +143,8 @@ export async function customModelRows({ type, cachedOnly }, { modelsDir, loaded
|
|
|
139
143
|
isLoaded: loaded.has(entry.name),
|
|
140
144
|
supportsImages: addon === 'llm' && Boolean(entry.projectionSrc),
|
|
141
145
|
expectedSize: entry.expectedSize ?? 0,
|
|
142
|
-
label: entry.label
|
|
146
|
+
label: entry.label,
|
|
147
|
+
taskType: addonTaskType(addon)
|
|
143
148
|
};
|
|
144
149
|
}));
|
|
145
150
|
return cachedOnly ? rows.filter((row) => row.isCached) : rows;
|
|
@@ -75,6 +75,7 @@ declare class ModelPuddle extends ResourcePuddle<string, ModelHandle> {
|
|
|
75
75
|
readonly resources: Map<string, PuddleEntry>;
|
|
76
76
|
}
|
|
77
77
|
type PoolLike = ModelPuddle;
|
|
78
|
+
export declare function modelOf(key: string): string;
|
|
78
79
|
export declare function releaseModel(pool: PoolLike, key: string): void;
|
|
79
80
|
export declare function loadedModels(pool: PoolLike): LoadedModel[];
|
|
80
81
|
export interface ResidentLease {
|
|
@@ -86,6 +87,7 @@ export declare function leaseResidentModel(pool: PoolLike, wanted?: (model: stri
|
|
|
86
87
|
export declare function isResident(pool: PoolLike, key: string): boolean;
|
|
87
88
|
export declare function leaseCount(pool: PoolLike, key: string): number;
|
|
88
89
|
export declare function evictIdle(pool: PoolLike): Promise<void>;
|
|
90
|
+
export declare function evictIdleWhere(pool: PoolLike, wanted: (key: string) => boolean): Promise<void>;
|
|
89
91
|
export interface DeadlineOptions {
|
|
90
92
|
deadlineMs: number;
|
|
91
93
|
tickMs?: number;
|
|
@@ -95,9 +97,11 @@ export interface ModelCapacityOptions extends DeadlineOptions {
|
|
|
95
97
|
signal?: {
|
|
96
98
|
aborted?: boolean;
|
|
97
99
|
} | undefined;
|
|
100
|
+
evictable?: (key: string) => boolean;
|
|
98
101
|
}
|
|
99
|
-
export declare function awaitModelCapacity(pool: PoolLike, { cap, deadlineMs, tickMs, signal }: ModelCapacityOptions): Promise<boolean>;
|
|
100
|
-
export declare function evictModelWithin(pool: PoolLike, key: string,
|
|
102
|
+
export declare function awaitModelCapacity(pool: PoolLike, { cap, deadlineMs, tickMs, signal, evictable }: ModelCapacityOptions): Promise<boolean>;
|
|
103
|
+
export declare function evictModelWithin(pool: PoolLike, key: string, options: DeadlineOptions): Promise<boolean>;
|
|
104
|
+
export declare function evictModel(pool: PoolLike, model: string, options: DeadlineOptions): Promise<boolean>;
|
|
101
105
|
export type LoadSdk = () => Promise<ModelLifecycleSdk>;
|
|
102
106
|
export type EmbedLifecycleSdk = Pick<SdkModule, 'unloadModel' | 'modelRegistrySearch'> & Partial<CancelSdk> & {
|
|
103
107
|
loadModel(params: {
|
|
@@ -69,6 +69,9 @@ class ModelPuddle extends ResourcePuddle {
|
|
|
69
69
|
function entryOf(pool, key) {
|
|
70
70
|
return pool.resources.get(key);
|
|
71
71
|
}
|
|
72
|
+
export function modelOf(key) {
|
|
73
|
+
return key.split('\0')[0];
|
|
74
|
+
}
|
|
72
75
|
export function releaseModel(pool, key) {
|
|
73
76
|
if (pool.destroyed)
|
|
74
77
|
return;
|
|
@@ -123,8 +126,43 @@ export async function evictIdle(pool) {
|
|
|
123
126
|
await pool.gc();
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
|
-
export async function
|
|
127
|
-
|
|
129
|
+
export async function evictIdleWhere(pool, wanted) {
|
|
130
|
+
for (let tick = 0; tick <= pool.gcIdleTicks; tick++) {
|
|
131
|
+
if (pool.destroyed)
|
|
132
|
+
return;
|
|
133
|
+
const held = [];
|
|
134
|
+
for (const entry of pool.resources.values()) {
|
|
135
|
+
if (entry.refs === 0 && !wanted(entry.id)) {
|
|
136
|
+
held.push(entry.id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (pool.idle === held.length) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
for (const key of held) {
|
|
143
|
+
pool.open(key);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
await pool.gc();
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
for (const key of held) {
|
|
150
|
+
releaseSettled(pool, key);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function residentCount(pool, wanted) {
|
|
156
|
+
let count = 0;
|
|
157
|
+
for (const entry of pool.resources.values()) {
|
|
158
|
+
if (wanted(entry.id) && entry.value.peek() !== null) {
|
|
159
|
+
count++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return count;
|
|
163
|
+
}
|
|
164
|
+
export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250, signal, evictable = () => true }) {
|
|
165
|
+
const roomed = () => residentCount(pool, evictable) < cap;
|
|
128
166
|
let done = false;
|
|
129
167
|
let timer = null;
|
|
130
168
|
const expired = new Promise((resolve) => {
|
|
@@ -134,7 +172,7 @@ export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250,
|
|
|
134
172
|
}, deadlineMs);
|
|
135
173
|
});
|
|
136
174
|
const evicting = (async () => {
|
|
137
|
-
await
|
|
175
|
+
await evictIdleWhere(pool, evictable);
|
|
138
176
|
while (!roomed()) {
|
|
139
177
|
if (done || signal?.aborted)
|
|
140
178
|
return false;
|
|
@@ -142,7 +180,7 @@ export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250,
|
|
|
142
180
|
if (done || signal?.aborted) {
|
|
143
181
|
return false;
|
|
144
182
|
}
|
|
145
|
-
await
|
|
183
|
+
await evictIdleWhere(pool, evictable);
|
|
146
184
|
}
|
|
147
185
|
return true;
|
|
148
186
|
})();
|
|
@@ -156,18 +194,36 @@ export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250,
|
|
|
156
194
|
}
|
|
157
195
|
}
|
|
158
196
|
}
|
|
159
|
-
export
|
|
197
|
+
export function evictModelWithin(pool, key, options) {
|
|
198
|
+
return evictWhereWithin(pool, (candidate) => candidate === key, options);
|
|
199
|
+
}
|
|
200
|
+
export function evictModel(pool, model, options) {
|
|
201
|
+
return evictWhereWithin(pool, (key) => modelOf(key) === model, options);
|
|
202
|
+
}
|
|
203
|
+
async function evictWhereWithin(pool, wanted, { deadlineMs, tickMs = 250 }) {
|
|
204
|
+
let done = false;
|
|
160
205
|
const evicting = (async () => {
|
|
161
|
-
while (
|
|
162
|
-
await
|
|
163
|
-
if (
|
|
206
|
+
while (residentCount(pool, wanted) > 0) {
|
|
207
|
+
await evictIdleWhere(pool, wanted);
|
|
208
|
+
if (residentCount(pool, wanted) === 0) {
|
|
164
209
|
break;
|
|
165
210
|
}
|
|
166
211
|
await new Promise((resolve) => setTimeout(resolve, tickMs));
|
|
212
|
+
if (done) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
167
215
|
}
|
|
168
216
|
return true;
|
|
169
217
|
})();
|
|
170
|
-
|
|
218
|
+
try {
|
|
219
|
+
return await raceTimeout(evicting, { ms: deadlineMs, fallback: false });
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
done = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function definedFields(config) {
|
|
226
|
+
return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined));
|
|
171
227
|
}
|
|
172
228
|
function llmVerbosity() {
|
|
173
229
|
const raw = Number(process.env.QVAC_LLM_VERBOSITY ?? 0);
|
|
@@ -253,7 +309,7 @@ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = cr
|
|
|
253
309
|
const openclCache = deviceConfig.device !== 'cpu' && deviceConfig.openclCacheDir
|
|
254
310
|
? deviceConfig.openclCacheDir()
|
|
255
311
|
: undefined;
|
|
256
|
-
const modelConfig = {
|
|
312
|
+
const modelConfig = definedFields({
|
|
257
313
|
verbosity: llmVerbosity(),
|
|
258
314
|
tools: true,
|
|
259
315
|
stop_sequences: RUNAWAY_STOP_SEQUENCES,
|
|
@@ -265,7 +321,7 @@ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = cr
|
|
|
265
321
|
gpu_layers: deviceConfig.gpuLayers,
|
|
266
322
|
'main-gpu': mainGpu,
|
|
267
323
|
openclCacheDir: openclCache || undefined
|
|
268
|
-
};
|
|
324
|
+
});
|
|
269
325
|
const { projectionModelSrc: projection, ...loggedConfig } = modelConfig;
|
|
270
326
|
hlog('llm load config', { model, projection: Boolean(projection), ...loggedConfig });
|
|
271
327
|
const loadStarted = Date.now();
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ModelAddon } from './model-source.ts';
|
|
2
|
+
export type ModelTaskType = 'llm' | 'embeddings' | 'speech-to-text' | 'text-to-speech' | 'image-generation' | 'audio-generation' | 'translation' | 'text-recognition' | 'other';
|
|
3
|
+
export declare function addonTaskType(addon: ModelAddon): ModelTaskType;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const ADDON_TASK_TYPES = {
|
|
2
|
+
llm: 'llm',
|
|
3
|
+
embeddings: 'embeddings',
|
|
4
|
+
whisper: 'speech-to-text',
|
|
5
|
+
parakeet: 'speech-to-text',
|
|
6
|
+
tts: 'text-to-speech',
|
|
7
|
+
diffusion: 'image-generation',
|
|
8
|
+
audiogen: 'audio-generation',
|
|
9
|
+
nmt: 'translation',
|
|
10
|
+
ocr: 'text-recognition'
|
|
11
|
+
};
|
|
12
|
+
export function addonTaskType(addon) {
|
|
13
|
+
return ADDON_TASK_TYPES[addon] ?? 'other';
|
|
14
|
+
}
|
|
@@ -10,3 +10,5 @@ export interface MusicChoiceEntry {
|
|
|
10
10
|
readonly notes: string;
|
|
11
11
|
}
|
|
12
12
|
export declare const MUSIC_CHOICE_CATALOG: ReadonlyArray<MusicChoiceEntry>;
|
|
13
|
+
export declare function musicModelChoiceFrom(settings: unknown): MusicModelChoice | undefined;
|
|
14
|
+
export declare function isMusicModelChoice(value: unknown): value is MusicModelChoice;
|
|
@@ -24,3 +24,12 @@ export const MUSIC_CHOICE_CATALOG = [
|
|
|
24
24
|
notes: 'Fine-tuned quality. Wants a desktop GPU.'
|
|
25
25
|
}
|
|
26
26
|
];
|
|
27
|
+
export function musicModelChoiceFrom(settings) {
|
|
28
|
+
if (!settings || typeof settings !== 'object' || !('modelChoice' in settings)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return isMusicModelChoice(settings.modelChoice) ? settings.modelChoice : undefined;
|
|
32
|
+
}
|
|
33
|
+
export function isMusicModelChoice(value) {
|
|
34
|
+
return MUSIC_MODEL_CHOICES.some((choice) => choice === value);
|
|
35
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AbortSignal } from 'bare-abort-controller';
|
|
2
|
-
export { closePythonWorker as closeOfficeWorker } from '../tools/exec/adapters/python/worker/
|
|
2
|
+
export { closePythonWorker as closeOfficeWorker } from '../tools/exec/adapters/python/worker/active.ts';
|
|
3
3
|
export interface OfficeExtractOptions {
|
|
4
4
|
readonly signal?: AbortSignal | undefined;
|
|
5
5
|
readonly mimeType?: string | undefined;
|
|
@@ -3,7 +3,7 @@ import { extractDocxText } from '../docx/index.js';
|
|
|
3
3
|
import { extractWithPython } from './python.js';
|
|
4
4
|
import { XLSX_EXTRACT } from './xlsx.js';
|
|
5
5
|
import { PPTX_EXTRACT } from './pptx.js';
|
|
6
|
-
export { closePythonWorker as closeOfficeWorker } from '../tools/exec/adapters/python/worker/
|
|
6
|
+
export { closePythonWorker as closeOfficeWorker } from '../tools/exec/adapters/python/worker/active.js';
|
|
7
7
|
const OFFICE_MIME_EXTENSIONS = {
|
|
8
8
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
|
|
9
9
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
|
|
@@ -10,8 +10,7 @@ export async function extractWithPython(bytes, fileName, { format, packages, sou
|
|
|
10
10
|
const dir = wheelsDir();
|
|
11
11
|
const manifest = vendoredManifest(dir);
|
|
12
12
|
const install = planVendoredInstall(packages, { dir, manifest });
|
|
13
|
-
if (!
|
|
14
|
-
!manifest ||
|
|
13
|
+
if (!manifest ||
|
|
15
14
|
!install ||
|
|
16
15
|
!['micropip', ...install.packages].every((name) => Object.keys(manifest.files).some((file) => file.replaceAll('_', '-').startsWith(`${name}-`) && fs.existsSync(`${dir}/${file}`)))) {
|
|
17
16
|
throw new Error(`bundled ${format} extraction packages are unavailable`);
|
package/dist-lib/lib/serve.js
CHANGED
|
@@ -191,6 +191,18 @@ export function serveHarness(stream, harness) {
|
|
|
191
191
|
await harness.warmModel({ model, agent: agent ?? {} });
|
|
192
192
|
return { ok: true };
|
|
193
193
|
},
|
|
194
|
+
loadModel: async ({ model, agent }) => {
|
|
195
|
+
if (!harness.loadModel) {
|
|
196
|
+
throw new Error('harness does not support loadModel');
|
|
197
|
+
}
|
|
198
|
+
return harness.loadModel({ model, agent: agent ?? {} });
|
|
199
|
+
},
|
|
200
|
+
unloadModel: async ({ model }) => {
|
|
201
|
+
if (!harness.unloadModel) {
|
|
202
|
+
throw new Error('harness does not support unloadModel');
|
|
203
|
+
}
|
|
204
|
+
return harness.unloadModel({ model });
|
|
205
|
+
},
|
|
194
206
|
modelEvents: () => modelEvents(harness),
|
|
195
207
|
completion: completion(harness)
|
|
196
208
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface DeadlineData {
|
|
2
|
+
readonly state: SharedArrayBuffer;
|
|
3
|
+
readonly interrupt: SharedArrayBuffer;
|
|
4
|
+
readonly ms: number;
|
|
5
|
+
}
|
|
6
|
+
export declare const ARMED = 0;
|
|
7
|
+
export declare const DISARMED = 1;
|
|
8
|
+
export declare const FIRED = 2;
|
|
9
|
+
export declare const SIGINT = 2;
|
|
10
|
+
export declare const RESEND_MS = 100;
|
|
@@ -1,28 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const RESEND_MS = 100;
|
|
7
|
-
const SOURCE = `
|
|
8
|
-
const state = new Int32Array(data.state)
|
|
9
|
-
const interrupt = new Uint8Array(data.interrupt)
|
|
10
|
-
Atomics.wait(state, 0, ${ARMED}, data.ms)
|
|
11
|
-
if (Atomics.compareExchange(state, 0, ${ARMED}, ${FIRED}) === ${ARMED}) {
|
|
12
|
-
do Atomics.store(interrupt, 0, ${SIGINT})
|
|
13
|
-
while (Atomics.wait(state, 0, ${FIRED}, ${RESEND_MS}) === 'timed-out')
|
|
14
|
-
}
|
|
15
|
-
`;
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import { Worker } from 'worker_threads';
|
|
3
|
+
import { errorMessage } from '../../../../guards.js';
|
|
4
|
+
import { hlog } from '../../../../log.js';
|
|
5
|
+
import { DISARMED, FIRED } from './deadline-state.js';
|
|
16
6
|
export function startInterruptTimer(ms) {
|
|
17
7
|
const state = new Int32Array(new SharedArrayBuffer(4));
|
|
18
8
|
const interrupt = new Uint8Array(new SharedArrayBuffer(1));
|
|
19
|
-
const
|
|
9
|
+
const workerData = { state: state.buffer, interrupt: interrupt.buffer, ms };
|
|
10
|
+
const entry = fileURLToPath(import.meta.resolve('#pyodide-interrupt-deadline'));
|
|
11
|
+
new Worker(entry, { workerData })
|
|
12
|
+
.on('error', (error) => hlog('python: interrupt deadline failed', { err: errorMessage(error) }))
|
|
13
|
+
.unref();
|
|
20
14
|
return {
|
|
21
15
|
interrupt,
|
|
22
16
|
disarm() {
|
|
23
17
|
const fired = Atomics.exchange(state, 0, DISARMED) === FIRED;
|
|
24
18
|
Atomics.notify(state, 0);
|
|
25
|
-
reap();
|
|
26
19
|
return fired;
|
|
27
20
|
}
|
|
28
21
|
};
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
1
|
import b4a from 'b4a';
|
|
10
2
|
import { constants as fsConstants } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
11
4
|
import process from 'process';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
12
6
|
import { createGuardedFetch } from './guarded-fetch.js';
|
|
13
7
|
import { seedPackageCache } from './wheels.js';
|
|
14
8
|
class B4aTextDecoder {
|
|
@@ -95,15 +89,25 @@ export function pyodideLoadOptions() {
|
|
|
95
89
|
}
|
|
96
90
|
return { ...silentStdio, packageCacheDir: cacheDir };
|
|
97
91
|
}
|
|
92
|
+
function pyodideIndexURL() {
|
|
93
|
+
import.meta.asset('pyodide/python_stdlib.zip');
|
|
94
|
+
import.meta.asset('pyodide/pyodide-lock.json');
|
|
95
|
+
return path.dirname(fileURLToPath(import.meta.asset('pyodide/pyodide.asm.wasm'))) + '/';
|
|
96
|
+
}
|
|
98
97
|
export const defaultLoad = async () => {
|
|
99
98
|
if (typeof globalThis.Bare !== 'undefined') {
|
|
100
99
|
await shapeBareGlobals();
|
|
101
100
|
}
|
|
102
|
-
const
|
|
103
|
-
const {
|
|
101
|
+
const { loadPyodide } = await import('pyodide');
|
|
102
|
+
const { default: createPyodideModule } = await import('pyodide/pyodide.asm.mjs');
|
|
104
103
|
const options = pyodideLoadOptions();
|
|
105
104
|
if (options.packageCacheDir) {
|
|
106
105
|
seedPackageCache(options.packageCacheDir);
|
|
107
106
|
}
|
|
108
|
-
|
|
107
|
+
const indexURL = pyodideIndexURL();
|
|
108
|
+
return (await loadPyodide({
|
|
109
|
+
...options,
|
|
110
|
+
indexURL,
|
|
111
|
+
createPyodideModule
|
|
112
|
+
}));
|
|
109
113
|
};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { AbortController } from 'bare-abort-controller';
|
|
2
|
-
import process from 'process';
|
|
3
2
|
import ScopeLock from 'scope-lock';
|
|
4
3
|
import { errorMessage } from '../../../../guards.js';
|
|
5
4
|
import { AbortedError, raceAbort } from '../../../../helpers/race-abort.js';
|
|
6
5
|
import { hlog } from '../../../../log.js';
|
|
7
6
|
import { MAX_OUTPUT_CHARS } from '../../runner.js';
|
|
8
7
|
import { startInterruptTimer } from './interrupt-timer.js';
|
|
9
|
-
import {
|
|
8
|
+
import { pyodideLoadOptions } from './loader.js';
|
|
10
9
|
import { isPyodideFatal, performRun } from './run-session.js';
|
|
11
10
|
import { runPythonInWorker } from './worker/client.js';
|
|
12
11
|
export { pyodideLoadOptions };
|
|
@@ -98,11 +97,6 @@ async function runInProcess(load, spec, options) {
|
|
|
98
97
|
stop.abort(undefined);
|
|
99
98
|
}
|
|
100
99
|
}
|
|
101
|
-
function workerAvailable() {
|
|
102
|
-
if (process.env.QVAC_PYODIDE_WORKER_ENTRY)
|
|
103
|
-
return true;
|
|
104
|
-
return !import.meta.url.includes('.bundle/');
|
|
105
|
-
}
|
|
106
100
|
export async function runPython(code, options = {}) {
|
|
107
101
|
if (options.signal?.aborted)
|
|
108
102
|
throw new AbortedError();
|
|
@@ -126,14 +120,6 @@ export async function runPython(code, options = {}) {
|
|
|
126
120
|
askConnect: options.askConnect
|
|
127
121
|
});
|
|
128
122
|
}
|
|
129
|
-
if (!workerAvailable()) {
|
|
130
|
-
hlog('python: worker entry unavailable in this bundle; running in-process');
|
|
131
|
-
return runInProcess(defaultLoad, spec, {
|
|
132
|
-
timeoutMs,
|
|
133
|
-
signal: options.signal,
|
|
134
|
-
askConnect: options.askConnect
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
123
|
await runLock.lock();
|
|
138
124
|
try {
|
|
139
125
|
if (options.signal?.aborted) {
|
|
@@ -12,6 +12,7 @@ export declare function pinned(spec: string): string;
|
|
|
12
12
|
export declare function pipMessage(targets: readonly string[] | null, { sandbox }?: {
|
|
13
13
|
sandbox?: boolean | undefined;
|
|
14
14
|
}): string;
|
|
15
|
+
export declare function sandboxInstallError(command: string, declared: readonly string[] | undefined): string | null;
|
|
15
16
|
export interface PipFromSourceHintOptions {
|
|
16
17
|
readonly packages?: readonly string[] | undefined;
|
|
17
18
|
readonly sandbox?: boolean;
|
|
@@ -242,6 +242,16 @@ export function pipMessage(targets, { sandbox = false } = {}) {
|
|
|
242
242
|
const declare = specs ? `packages: ${JSON.stringify(specs)}` : 'the PyPI names in packages';
|
|
243
243
|
return `pip is not available. Call exec with language "python", ${declare} and the Python source in command. Installing and running are the same one call.`;
|
|
244
244
|
}
|
|
245
|
+
export function sandboxInstallError(command, declared) {
|
|
246
|
+
const install = pipInstallTargets(command)?.map(pinned) ?? null;
|
|
247
|
+
const packages = [...new Set([...(declared ?? []), ...(install ?? [])])];
|
|
248
|
+
if (packages.length === 0 || planVendoredInstall(packages)) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
return install
|
|
252
|
+
? pipMessage(install, { sandbox: true })
|
|
253
|
+
: 'sandbox mode: installing packages reaches the network';
|
|
254
|
+
}
|
|
245
255
|
const PIP_CALL = /\b(?:subprocess\.\w+|os\.(?:system|popen)|Popen|check_(?:output|call))\s*\(/;
|
|
246
256
|
const STRING_LITERAL = /"([^"\n]*)"|'([^'\n]*)'/g;
|
|
247
257
|
function pipLineTargets(line) {
|
|
@@ -1279,8 +1289,9 @@ export function createPythonAdapter(disabled, loadPyodide, network = false) {
|
|
|
1279
1289
|
const packages = install
|
|
1280
1290
|
? [...new Set([...(prepared.packages ?? []), ...install])]
|
|
1281
1291
|
: prepared.packages;
|
|
1282
|
-
|
|
1283
|
-
|
|
1292
|
+
const refusal = sandbox ? sandboxInstallError(command, prepared.packages) : null;
|
|
1293
|
+
if (refusal) {
|
|
1294
|
+
return fail(refusal);
|
|
1284
1295
|
}
|
|
1285
1296
|
const result = await runPython(install ? 'pass' : source, {
|
|
1286
1297
|
timeoutMs: options.timeoutMs,
|
|
@@ -28,8 +28,8 @@ interface WheelFs {
|
|
|
28
28
|
exists: boolean;
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
export declare function wheelsDir(): string
|
|
32
|
-
export declare function vendoredManifest(dir?: string
|
|
31
|
+
export declare function wheelsDir(): string;
|
|
32
|
+
export declare function vendoredManifest(dir?: string): WheelManifest | null;
|
|
33
33
|
export declare function normalizeDistribution(name: string): string;
|
|
34
34
|
export interface VendoredOptions {
|
|
35
35
|
readonly dir?: string | undefined;
|
|
@@ -38,5 +38,5 @@ export interface VendoredOptions {
|
|
|
38
38
|
export declare function vendoredSpec(distribution: string, opts?: VendoredOptions): string | null;
|
|
39
39
|
export declare function planVendoredInstall(specs: readonly string[], opts?: VendoredOptions): VendoredInstall | null;
|
|
40
40
|
export declare function stageVendoredWheels(target: WheelFs, install: VendoredInstall): string[];
|
|
41
|
-
export declare function seedPackageCache(cacheDir: string, dir?: string
|
|
41
|
+
export declare function seedPackageCache(cacheDir: string, dir?: string): void;
|
|
42
42
|
export {};
|
|
@@ -16,20 +16,9 @@ const ManifestSchema = z.object({
|
|
|
16
16
|
});
|
|
17
17
|
const STAGE_DIR = '/qvac-wheels';
|
|
18
18
|
export function wheelsDir() {
|
|
19
|
-
|
|
20
|
-
if (fromHost) {
|
|
21
|
-
return fromHost;
|
|
22
|
-
}
|
|
23
|
-
const root = new URL('../../../../../', import.meta.url);
|
|
24
|
-
const source = new URL(root.pathname.endsWith('/dist-lib/') ? '../wheels' : 'wheels', root);
|
|
25
|
-
if (source.href.includes('.bundle/')) {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
return fileURLToPath(source.href);
|
|
19
|
+
return process.env.QVAC_PYTHON_WHEELS_DIR || fileURLToPath(import.meta.asset('#pyodide-wheels'));
|
|
29
20
|
}
|
|
30
21
|
export function vendoredManifest(dir = wheelsDir()) {
|
|
31
|
-
if (!dir)
|
|
32
|
-
return null;
|
|
33
22
|
const file = `${dir}/manifest.json`;
|
|
34
23
|
if (!fs.existsSync(file)) {
|
|
35
24
|
hlog('python: no vendored wheels', { dir });
|
|
@@ -70,7 +59,7 @@ export function vendoredSpec(distribution, opts = {}) {
|
|
|
70
59
|
export function planVendoredInstall(specs, opts = {}) {
|
|
71
60
|
const dir = opts.dir ?? wheelsDir();
|
|
72
61
|
const manifest = opts.manifest === undefined ? vendoredManifest(dir) : opts.manifest;
|
|
73
|
-
if (!manifest ||
|
|
62
|
+
if (!manifest || specs.length === 0) {
|
|
74
63
|
return null;
|
|
75
64
|
}
|
|
76
65
|
const wheels = new Set();
|
|
@@ -108,7 +97,7 @@ export function stageVendoredWheels(target, install) {
|
|
|
108
97
|
}
|
|
109
98
|
export function seedPackageCache(cacheDir, dir = wheelsDir()) {
|
|
110
99
|
const manifest = vendoredManifest(dir);
|
|
111
|
-
if (!manifest
|
|
100
|
+
if (!manifest) {
|
|
112
101
|
return;
|
|
113
102
|
}
|
|
114
103
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type WorkerMessage } from './protocol.ts';
|
|
2
|
+
import type { WorkerLink } from './spawn.ts';
|
|
3
|
+
export interface ActiveWorker {
|
|
4
|
+
readonly link: WorkerLink;
|
|
5
|
+
readonly view: Uint8Array;
|
|
6
|
+
readonly ready: Promise<void>;
|
|
7
|
+
nextId: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function activeWorker(boot: () => ActiveWorker): ActiveWorker;
|
|
10
|
+
export declare function forget(worker: ActiveWorker): void;
|
|
11
|
+
export declare function destroy(worker: ActiveWorker): void;
|
|
12
|
+
export declare function readMessage(link: WorkerLink): Promise<WorkerMessage | null>;
|
|
13
|
+
export declare function drainAndReap(worker: ActiveWorker): Promise<void>;
|
|
14
|
+
export interface Grace {
|
|
15
|
+
expired: Promise<'grace'>;
|
|
16
|
+
clear: () => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function grace(ms: number): Grace;
|
|
19
|
+
export declare function closePythonWorker(): Promise<void>;
|