@qvac/harness 0.6.0 → 0.7.1
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 +132 -1
- package/README.md +47 -29
- package/dist-lib/index.d.ts +4 -0
- package/dist-lib/index.js +31 -19
- package/dist-lib/lib/capabilities/preview-model-source.d.ts +0 -1
- package/dist-lib/lib/connect.js +5 -2
- package/dist-lib/lib/contract-types.d.ts +1 -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 +14 -6
- package/dist-lib/lib/models/model-catalog.js +14 -6
- package/dist-lib/lib/models/model-handle.d.ts +5 -1
- package/dist-lib/lib/models/model-handle.js +8 -3
- package/dist-lib/lib/models/model-list.d.ts +7 -3
- package/dist-lib/lib/models/model-list.js +9 -4
- package/dist-lib/lib/models/model-pool.d.ts +4 -1
- package/dist-lib/lib/models/model-pool.js +33 -10
- 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 +1 -1
- 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 +4 -2
- package/dist-lib/spec/hrpc/messages.js +11 -8
- package/dist-lib/spec/hyperschema/index.js +11 -8
- package/dist-lib/spec/hyperschema/schema.json +11 -1
- package/dist-lib/spec/hyperschema/types.d.ts +3 -1
- package/package.json +39 -20
- package/dist/harness.bundle +0 -42313
- 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
|
}
|
|
@@ -5,13 +5,17 @@ export interface ModelResource {
|
|
|
5
5
|
}
|
|
6
6
|
export interface ModelHandleOptions {
|
|
7
7
|
load(): Promise<ModelResource>;
|
|
8
|
+
onLoaded?(): void;
|
|
9
|
+
onFailed?(err: unknown): void;
|
|
8
10
|
}
|
|
9
11
|
export declare class ModelHandle {
|
|
10
12
|
private readonly _load;
|
|
11
13
|
private readonly _peek;
|
|
14
|
+
private readonly _onLoaded;
|
|
15
|
+
private readonly _onFailed;
|
|
12
16
|
private _tracked;
|
|
13
17
|
private _loading;
|
|
14
|
-
constructor({ load }: ModelHandleOptions);
|
|
18
|
+
constructor({ load, onLoaded, onFailed }: ModelHandleOptions);
|
|
15
19
|
load(): Promise<ModelResource>;
|
|
16
20
|
get loading(): boolean;
|
|
17
21
|
peek(): Promise<ModelResource> | null;
|
|
@@ -2,12 +2,16 @@ import { retryMemo } from '../helpers/retry-memo.js';
|
|
|
2
2
|
export class ModelHandle {
|
|
3
3
|
_load;
|
|
4
4
|
_peek;
|
|
5
|
+
_onLoaded;
|
|
6
|
+
_onFailed;
|
|
5
7
|
_tracked = null;
|
|
6
8
|
_loading = false;
|
|
7
|
-
constructor({ load }) {
|
|
9
|
+
constructor({ load, onLoaded, onFailed }) {
|
|
8
10
|
const memo = retryMemo(load);
|
|
9
11
|
this._load = memo;
|
|
10
12
|
this._peek = memo.peek;
|
|
13
|
+
this._onLoaded = onLoaded;
|
|
14
|
+
this._onFailed = onFailed;
|
|
11
15
|
this.load().catch(noop);
|
|
12
16
|
}
|
|
13
17
|
load() {
|
|
@@ -15,12 +19,13 @@ export class ModelHandle {
|
|
|
15
19
|
if (attempt !== this._tracked) {
|
|
16
20
|
this._tracked = attempt;
|
|
17
21
|
this._loading = true;
|
|
18
|
-
const
|
|
22
|
+
const settle = (report) => {
|
|
19
23
|
if (this._tracked === attempt) {
|
|
20
24
|
this._loading = false;
|
|
25
|
+
report();
|
|
21
26
|
}
|
|
22
27
|
};
|
|
23
|
-
attempt.then(
|
|
28
|
+
attempt.then(() => settle(() => this._onLoaded?.()), (err) => settle(() => this._onFailed?.(err)));
|
|
24
29
|
}
|
|
25
30
|
return attempt;
|
|
26
31
|
}
|
|
@@ -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;
|
|
@@ -15,11 +16,14 @@ export interface ModelInfo {
|
|
|
15
16
|
registrySource: string;
|
|
16
17
|
isCached: boolean;
|
|
17
18
|
isLoaded: boolean;
|
|
19
|
+
isLoading: boolean;
|
|
18
20
|
supportsImages: boolean;
|
|
19
21
|
expectedSize: number;
|
|
20
22
|
actualSize?: number;
|
|
21
23
|
label?: string;
|
|
24
|
+
taskType: ModelTaskType;
|
|
22
25
|
}
|
|
26
|
+
export type CatalogRow = Omit<ModelInfo, 'isLoading'>;
|
|
23
27
|
export interface ListModelsOptions {
|
|
24
28
|
type?: ModelType;
|
|
25
29
|
cachedOnly?: boolean;
|
|
@@ -43,11 +47,11 @@ export interface ListModelsRuntime {
|
|
|
43
47
|
cache?: ModelStatusCache | undefined;
|
|
44
48
|
projectionFor?: ((name: string) => string | undefined) | undefined;
|
|
45
49
|
}
|
|
46
|
-
export declare function listModels(sdk: ModelListSdk, { type, cachedOnly, fullRegistry }?: ListModelsOptions, { cache, projectionFor }?: ListModelsRuntime): Promise<
|
|
50
|
+
export declare function listModels(sdk: ModelListSdk, { type, cachedOnly, fullRegistry }?: ListModelsOptions, { cache, projectionFor }?: ListModelsRuntime): Promise<CatalogRow[]>;
|
|
47
51
|
export interface CustomModelRowsContext {
|
|
48
52
|
readonly modelsDir?: string;
|
|
49
53
|
readonly loaded: ReadonlySet<string>;
|
|
50
54
|
}
|
|
51
|
-
export declare function customModelRows({ type, cachedOnly }: ListModelsOptions, { modelsDir, loaded }: CustomModelRowsContext): Promise<
|
|
52
|
-
export declare function mergeCustomRows(catalog: ReadonlyArray<
|
|
55
|
+
export declare function customModelRows({ type, cachedOnly }: ListModelsOptions, { modelsDir, loaded }: CustomModelRowsContext): Promise<CatalogRow[]>;
|
|
56
|
+
export declare function mergeCustomRows(catalog: ReadonlyArray<CatalogRow>, custom: ReadonlyArray<CatalogRow>): CatalogRow[];
|
|
53
57
|
export {};
|
|
@@ -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;
|
|
@@ -5,8 +5,9 @@ import { type ModelAddon } from './model-source.ts';
|
|
|
5
5
|
import type { GpuFaultState } from './sdk-faults.ts';
|
|
6
6
|
type SdkModule = typeof import('@qvac/sdk');
|
|
7
7
|
export interface ModelState {
|
|
8
|
-
status: 'loading' | 'downloading' | 'downloaded' | 'ready' | 'unloading' | 'unloaded';
|
|
8
|
+
status: 'loading' | 'downloading' | 'downloaded' | 'ready' | 'error' | 'unloading' | 'unloaded';
|
|
9
9
|
model: string;
|
|
10
|
+
catalogChanged?: boolean;
|
|
10
11
|
[key: string]: unknown;
|
|
11
12
|
}
|
|
12
13
|
export type NotifyFn = (state: ModelState) => void;
|
|
@@ -78,6 +79,8 @@ type PoolLike = ModelPuddle;
|
|
|
78
79
|
export declare function modelOf(key: string): string;
|
|
79
80
|
export declare function releaseModel(pool: PoolLike, key: string): void;
|
|
80
81
|
export declare function loadedModels(pool: PoolLike): LoadedModel[];
|
|
82
|
+
export declare function settledModels(pool: PoolLike): string[];
|
|
83
|
+
export declare function loadingModels(pool: PoolLike): string[];
|
|
81
84
|
export interface ResidentLease {
|
|
82
85
|
model: string;
|
|
83
86
|
modelId: string;
|
|
@@ -10,6 +10,7 @@ import { hlog } from '../log.js';
|
|
|
10
10
|
import { RUNAWAY_STOP_SEQUENCES } from '../helpers/control-tokens.js';
|
|
11
11
|
import { raceTimeout } from '../helpers/race-timeout.js';
|
|
12
12
|
import { resolveModelSrc } from './model-source.js';
|
|
13
|
+
import { errorMessage } from '../guards.js';
|
|
13
14
|
export function createLoadTracker() {
|
|
14
15
|
const inflight = new Set();
|
|
15
16
|
let draining = null;
|
|
@@ -101,6 +102,34 @@ export function loadedModels(pool) {
|
|
|
101
102
|
}
|
|
102
103
|
return out;
|
|
103
104
|
}
|
|
105
|
+
export function settledModels(pool) {
|
|
106
|
+
const out = [];
|
|
107
|
+
for (const entry of pool.resources.values()) {
|
|
108
|
+
if (entry.value.peek() !== null && !entry.value.loading) {
|
|
109
|
+
out.push(modelOf(entry.id));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
export function loadingModels(pool) {
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const entry of pool.resources.values()) {
|
|
117
|
+
if (entry.value.loading) {
|
|
118
|
+
out.push(modelOf(entry.id));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
function reportedHandle(notify, model, load) {
|
|
124
|
+
return new ModelHandle({
|
|
125
|
+
load: () => {
|
|
126
|
+
notify({ status: 'loading', model, catalogChanged: true });
|
|
127
|
+
return load();
|
|
128
|
+
},
|
|
129
|
+
onLoaded: () => notify({ status: 'ready', model, catalogChanged: true }),
|
|
130
|
+
onFailed: (err) => notify({ status: 'error', model, message: errorMessage(err), catalogChanged: true })
|
|
131
|
+
});
|
|
132
|
+
}
|
|
104
133
|
export async function leaseResidentModel(pool, wanted = () => true) {
|
|
105
134
|
for (const entry of pool.resources.values()) {
|
|
106
135
|
const key = entry.id;
|
|
@@ -292,7 +321,6 @@ export function loadClockGuard(model, sdk, { now = () => Date.now(), monoMs = mo
|
|
|
292
321
|
export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = createLoadTracker() } = {}) {
|
|
293
322
|
const load = async (model, opts) => {
|
|
294
323
|
const sdk = await loadSdk();
|
|
295
|
-
notify({ status: 'loading', model });
|
|
296
324
|
const modelSrc = await resolveModelSrc(sdk, model);
|
|
297
325
|
const resolved = resolveLoadOptions(opts, deviceConfig);
|
|
298
326
|
const ctxSize = resolved.ctxSize || defaultContextSize(modelSrc, customContextLength(model));
|
|
@@ -332,11 +360,10 @@ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = cr
|
|
|
332
360
|
onProgress: (progress) => notify(progressState(model, progress))
|
|
333
361
|
})));
|
|
334
362
|
hlog('llm loaded', { model, loadMs: Date.now() - loadStarted });
|
|
335
|
-
notify({ status: 'ready', model });
|
|
336
363
|
return { model, modelId, ctxSize };
|
|
337
364
|
};
|
|
338
365
|
return new ModelPuddle({
|
|
339
|
-
open: (key, model = key, opts = {}) =>
|
|
366
|
+
open: (key, model = key, opts = {}) => reportedHandle(notify, model, () => load(model, opts)),
|
|
340
367
|
gc: unloadOnGc(loadSdk, notify),
|
|
341
368
|
...NO_IDLE_GC
|
|
342
369
|
});
|
|
@@ -344,7 +371,6 @@ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = cr
|
|
|
344
371
|
export function createEmbedPool(loadSdk, notify, { faults, batchSize = DEFAULT_EMBED_BATCH_SIZE, openclCacheDir, loads = createLoadTracker() }) {
|
|
345
372
|
const load = async (model) => {
|
|
346
373
|
const sdk = await loadSdk();
|
|
347
|
-
notify({ status: 'loading', model });
|
|
348
374
|
const modelSrc = await resolveModelSrc(sdk, model, 'embeddings');
|
|
349
375
|
const openclCache = faults.device !== 'cpu' && openclCacheDir ? openclCacheDir() : undefined;
|
|
350
376
|
const modelId = await loadClockGuard(model, sdk).race(loads.track(sdk.loadModel({
|
|
@@ -358,11 +384,10 @@ export function createEmbedPool(loadSdk, notify, { faults, batchSize = DEFAULT_E
|
|
|
358
384
|
},
|
|
359
385
|
onProgress: (progress) => notify(progressState(model, progress))
|
|
360
386
|
})));
|
|
361
|
-
notify({ status: 'ready', model });
|
|
362
387
|
return { model, modelId };
|
|
363
388
|
};
|
|
364
389
|
return new ModelPuddle({
|
|
365
|
-
open: (key, model = key) =>
|
|
390
|
+
open: (key, model = key) => reportedHandle(notify, model, () => load(model)),
|
|
366
391
|
gc: unloadOnGc(loadSdk, notify),
|
|
367
392
|
...NO_IDLE_GC
|
|
368
393
|
});
|
|
@@ -372,7 +397,6 @@ export function createVoicePool(loadSdk, notify, { modelType, addon, modelConfig
|
|
|
372
397
|
let gpuRefused = false;
|
|
373
398
|
const load = async (model) => {
|
|
374
399
|
const sdk = await loadSdk();
|
|
375
|
-
notify({ status: 'loading', model });
|
|
376
400
|
const modelSrc = await resolveModelSrc(sdk, model, addon);
|
|
377
401
|
const attempt = () => {
|
|
378
402
|
const config = gpuRefused ? { ...modelConfig, useGPU: false } : modelConfig;
|
|
@@ -391,11 +415,10 @@ export function createVoicePool(loadSdk, notify, { modelType, addon, modelConfig
|
|
|
391
415
|
gpuRefused = true;
|
|
392
416
|
return attempt();
|
|
393
417
|
}));
|
|
394
|
-
notify({ status: 'ready', model });
|
|
395
418
|
return { model, modelId };
|
|
396
419
|
};
|
|
397
420
|
return new ModelPuddle({
|
|
398
|
-
open: (key, model = key) =>
|
|
421
|
+
open: (key, model = key) => reportedHandle(notify, model, () => load(model)),
|
|
399
422
|
gc: unloadOnGc(loadSdk, notify),
|
|
400
423
|
...NO_IDLE_GC
|
|
401
424
|
});
|
|
@@ -423,7 +446,7 @@ function unloadOnGc(loadSdk, notify) {
|
|
|
423
446
|
catch {
|
|
424
447
|
continue;
|
|
425
448
|
}
|
|
426
|
-
notify({ status: 'unloaded', model });
|
|
449
|
+
notify({ status: 'unloaded', model, catalogChanged: true });
|
|
427
450
|
}
|
|
428
451
|
};
|
|
429
452
|
}
|
|
@@ -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
|
@@ -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 });
|