@aparte/provider-transformers 0.2.0-alpha.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/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/assets/worker-Bk-8pt3W.js +70 -0
- package/dist/assets/worker-Bk-8pt3W.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +432 -0
- package/dist/index.js.map +1 -0
- package/dist/worker.d.ts +21 -0
- package/dist/worker.d.ts.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aparté
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @aparte/provider-transformers
|
|
2
|
+
|
|
3
|
+
Run LLMs **100% in the browser** via [Transformers.js](https://huggingface.co/docs/transformers.js)
|
|
4
|
+
(WebGPU, with a WASM fallback) — no API, no key, no server. Inference runs off the main thread in
|
|
5
|
+
a **Web Worker**, streaming tokens into aparté.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @aparte/provider-transformers @huggingface/transformers
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@huggingface/transformers` is a **peer dependency** — you bring the version you want (it's heavy and
|
|
12
|
+
ships its own onnxruntime). `@aparte/core` is a **peer dependency**.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { AparteConfig, DirectTransport } from '@aparte/core';
|
|
16
|
+
import { TransformersProvider, registerModel } from '@aparte/provider-transformers';
|
|
17
|
+
|
|
18
|
+
registerModel({
|
|
19
|
+
id: 'onnx-community/Qwen2.5-0.5B-Instruct',
|
|
20
|
+
name: 'Qwen2.5 0.5B',
|
|
21
|
+
task: 'text-generation',
|
|
22
|
+
capabilities: ['streaming'],
|
|
23
|
+
dtype: 'q4',
|
|
24
|
+
});
|
|
25
|
+
AparteConfig.registerAIProvider(TransformersProvider);
|
|
26
|
+
AparteConfig.setTransport(new DirectTransport({ byok: true }));
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The provider owns its I/O (it runs inference locally), so `DirectTransport` just delegates to it.
|
|
30
|
+
Model weights download once and persist in the Cache API; `prepareModel` reports progress, and
|
|
31
|
+
`listCachedModels` / `deleteCachedModel` manage the on-disk cache.
|
|
32
|
+
|
|
33
|
+
> **Scope (v1):** generic text-generation streaming. Tool-calling for local models is
|
|
34
|
+
> model-specific and out of scope for now. Part of the
|
|
35
|
+
> [aparté](https://github.com/apartejs/aparte) monorepo. ESM-only.
|
|
36
|
+
> See the **Providers** guide in the docs for the full usage.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { env, InterruptableStoppingCriteria, TextStreamer, pipeline } from "@huggingface/transformers";
|
|
2
|
+
env.allowLocalModels = false;
|
|
3
|
+
env.useBrowserCache = true;
|
|
4
|
+
const ctx = self;
|
|
5
|
+
function post(message) {
|
|
6
|
+
ctx.postMessage(message);
|
|
7
|
+
}
|
|
8
|
+
let _current = null;
|
|
9
|
+
const _activeStops = /* @__PURE__ */ new Map();
|
|
10
|
+
async function ensurePipeline(modelId, dtype, device, id) {
|
|
11
|
+
if (_current?.modelId === modelId) return _current.pipe;
|
|
12
|
+
const opts = {
|
|
13
|
+
progress_callback: (p) => {
|
|
14
|
+
if (!id) return;
|
|
15
|
+
if (p.status === "progress") {
|
|
16
|
+
post({ type: "progress", id, status: "downloading", file: p.file, progress: Math.round(p.progress ?? 0) });
|
|
17
|
+
} else if (p.status === "done") {
|
|
18
|
+
post({ type: "progress", id, status: "loading", file: p.file });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
if (dtype) opts["dtype"] = dtype;
|
|
23
|
+
if (device && device !== "auto") opts["device"] = device;
|
|
24
|
+
const pipe = await pipeline("text-generation", modelId, opts);
|
|
25
|
+
_current = { modelId, pipe };
|
|
26
|
+
post({ type: "pipeline-ready", modelId });
|
|
27
|
+
return pipe;
|
|
28
|
+
}
|
|
29
|
+
async function handlePrepare(msg) {
|
|
30
|
+
try {
|
|
31
|
+
await ensurePipeline(msg.modelId, msg.dtype, msg.device, msg.id);
|
|
32
|
+
post({ type: "progress", id: msg.id, status: "ready" });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
post({ type: "prepare-error", id: msg.id, message: err?.message ?? "Failed to load model" });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function handleGenerate(msg) {
|
|
38
|
+
const stoppingCriteria = new InterruptableStoppingCriteria();
|
|
39
|
+
_activeStops.set(msg.id, stoppingCriteria);
|
|
40
|
+
try {
|
|
41
|
+
const pipe = await ensurePipeline(msg.modelId, msg.dtype, msg.device, msg.id);
|
|
42
|
+
const streamer = new TextStreamer(pipe.tokenizer, {
|
|
43
|
+
skip_prompt: true,
|
|
44
|
+
skip_special_tokens: true,
|
|
45
|
+
callback_function: (text) => {
|
|
46
|
+
if (text) post({ type: "gen-chunk", id: msg.id, chunkType: "text", delta: text });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const temperature = msg.options.temperature ?? 0;
|
|
50
|
+
await pipe(msg.messages, {
|
|
51
|
+
max_new_tokens: msg.options.maxTokens ?? 512,
|
|
52
|
+
do_sample: temperature > 0,
|
|
53
|
+
temperature: temperature > 0 ? temperature : void 0,
|
|
54
|
+
streamer,
|
|
55
|
+
stopping_criteria: stoppingCriteria
|
|
56
|
+
});
|
|
57
|
+
post({ type: "gen-done", id: msg.id });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
post({ type: "gen-error", id: msg.id, message: err?.message ?? "Generation failed" });
|
|
60
|
+
} finally {
|
|
61
|
+
_activeStops.delete(msg.id);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
ctx.addEventListener("message", (event) => {
|
|
65
|
+
const msg = event.data;
|
|
66
|
+
if (msg.type === "prepare") void handlePrepare(msg);
|
|
67
|
+
else if (msg.type === "generate") void handleGenerate(msg);
|
|
68
|
+
else if (msg.type === "cancel") _activeStops.get(msg.id)?.interrupt();
|
|
69
|
+
});
|
|
70
|
+
//# sourceMappingURL=worker-Bk-8pt3W.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-Bk-8pt3W.js","sources":["../src/worker.ts"],"sourcesContent":["/**\n * Generic Transformers.js inference worker.\n *\n * Runs entirely off the main thread. It holds ONE text-generation pipeline at a\n * time and speaks a tiny postMessage protocol with the provider on the main\n * thread (see `index.ts`):\n *\n * main → worker : { type: 'prepare', id, modelId, dtype?, device? }\n * { type: 'generate', id, modelId, messages, options, dtype?, device? }\n * worker → main : { type: 'progress', id, status, file?, progress? }\n * { type: 'prepare-error', id, message }\n * { type: 'pipeline-ready', modelId }\n * { type: 'gen-chunk', id, chunkType: 'text', delta }\n * { type: 'gen-done', id }\n * { type: 'gen-error', id, message }\n *\n * Deliberately generic: no vision, no low-level ORT session management, no\n * model-family specifics — just the high-level `pipeline()` + `TextStreamer`.\n */\n\nimport { pipeline, TextStreamer, InterruptableStoppingCriteria, env, type TextGenerationPipeline } from '@huggingface/transformers';\n\n// Fetch weights from the Hugging Face hub (not local paths) and cache them in the\n// browser Cache API — this is what `listCachedModels()` scans on the main thread.\nenv.allowLocalModels = false;\nenv.useBrowserCache = true;\n\n// DOM's `Worker` interface types `postMessage` + typed `addEventListener('message')`,\n// which is enough for the worker scope — avoids pulling the WebWorker lib (it clashes\n// with DOM's global `postMessage`).\nconst ctx = self as unknown as Worker;\n\ntype Dtype = string | Record<string, string>;\ntype Device = 'webgpu' | 'wasm' | 'auto';\ninterface GenOptions { maxTokens?: number; temperature?: number; seed?: number }\ntype SimpleMessage = { role: 'user' | 'assistant' | 'system'; content: string };\n\ntype InMessage =\n | { type: 'prepare'; id: string; modelId: string; dtype?: Dtype; device?: Device }\n | { type: 'generate'; id: string; modelId: string; messages: SimpleMessage[]; options: GenOptions; dtype?: Dtype; device?: Device }\n | { type: 'cancel'; id: string };\n\nfunction post(message: unknown): void {\n ctx.postMessage(message);\n}\n\nlet _current: { modelId: string; pipe: TextGenerationPipeline } | null = null;\n// Per-generate interrupts, so a consumer's stream-cancel actually STOPS the model\n// (not just detaches the reader) — otherwise generation runs to max_new_tokens\n// off-thread, wasting exactly the CPU/GPU/battery this provider exists to save.\nconst _activeStops = new Map<string, InterruptableStoppingCriteria>();\n\n/**\n * Ensure the pipeline for `modelId` is loaded, reusing the current one when it\n * matches. On a fresh load it forwards download progress (when `id` is given) and\n * announces `pipeline-ready`.\n */\nasync function ensurePipeline(modelId: string, dtype: Dtype | undefined, device: Device | undefined, id?: string): Promise<TextGenerationPipeline> {\n if (_current?.modelId === modelId) return _current.pipe;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const opts: Record<string, any> = {\n progress_callback: (p: { status?: string; file?: string; progress?: number }) => {\n if (!id) return;\n if (p.status === 'progress') {\n post({ type: 'progress', id, status: 'downloading', file: p.file, progress: Math.round(p.progress ?? 0) });\n } else if (p.status === 'done') {\n post({ type: 'progress', id, status: 'loading', file: p.file });\n }\n },\n };\n if (dtype) opts['dtype'] = dtype;\n if (device && device !== 'auto') opts['device'] = device;\n\n const pipe = await pipeline('text-generation', modelId, opts) as TextGenerationPipeline;\n _current = { modelId, pipe };\n post({ type: 'pipeline-ready', modelId });\n return pipe;\n}\n\nasync function handlePrepare(msg: Extract<InMessage, { type: 'prepare' }>): Promise<void> {\n try {\n await ensurePipeline(msg.modelId, msg.dtype, msg.device, msg.id);\n post({ type: 'progress', id: msg.id, status: 'ready' });\n } catch (err) {\n post({ type: 'prepare-error', id: msg.id, message: (err as Error)?.message ?? 'Failed to load model' });\n }\n}\n\nasync function handleGenerate(msg: Extract<InMessage, { type: 'generate' }>): Promise<void> {\n const stoppingCriteria = new InterruptableStoppingCriteria();\n _activeStops.set(msg.id, stoppingCriteria);\n try {\n const pipe = await ensurePipeline(msg.modelId, msg.dtype, msg.device, msg.id);\n\n const streamer = new TextStreamer(pipe.tokenizer, {\n skip_prompt: true,\n skip_special_tokens: true,\n callback_function: (text: string) => {\n if (text) post({ type: 'gen-chunk', id: msg.id, chunkType: 'text', delta: text });\n },\n });\n\n const temperature = msg.options.temperature ?? 0;\n await pipe(msg.messages, {\n max_new_tokens: msg.options.maxTokens ?? 512,\n do_sample: temperature > 0,\n temperature: temperature > 0 ? temperature : undefined,\n streamer,\n stopping_criteria: stoppingCriteria,\n });\n\n post({ type: 'gen-done', id: msg.id });\n } catch (err) {\n post({ type: 'gen-error', id: msg.id, message: (err as Error)?.message ?? 'Generation failed' });\n } finally {\n _activeStops.delete(msg.id);\n }\n}\n\nctx.addEventListener('message', (event: MessageEvent<InMessage>) => {\n const msg = event.data;\n if (msg.type === 'prepare') void handlePrepare(msg);\n else if (msg.type === 'generate') void handleGenerate(msg);\n else if (msg.type === 'cancel') _activeStops.get(msg.id)?.interrupt();\n});\n"],"names":[],"mappings":";AAwBA,IAAI,mBAAmB;AACvB,IAAI,kBAAkB;AAKtB,MAAM,MAAM;AAYZ,SAAS,KAAK,SAAwB;AAClC,MAAI,YAAY,OAAO;AAC3B;AAEA,IAAI,WAAqE;AAIzE,MAAM,mCAAmB,IAAA;AAOzB,eAAe,eAAe,SAAiB,OAA0B,QAA4B,IAA8C;AAC/I,MAAI,UAAU,YAAY,QAAS,QAAO,SAAS;AAGnD,QAAM,OAA4B;AAAA,IAC9B,mBAAmB,CAAC,MAA6D;AAC7E,UAAI,CAAC,GAAI;AACT,UAAI,EAAE,WAAW,YAAY;AACzB,aAAK,EAAE,MAAM,YAAY,IAAI,QAAQ,eAAe,MAAM,EAAE,MAAM,UAAU,KAAK,MAAM,EAAE,YAAY,CAAC,GAAG;AAAA,MAC7G,WAAW,EAAE,WAAW,QAAQ;AAC5B,aAAK,EAAE,MAAM,YAAY,IAAI,QAAQ,WAAW,MAAM,EAAE,MAAM;AAAA,MAClE;AAAA,IACJ;AAAA,EAAA;AAEJ,MAAI,MAAO,MAAK,OAAO,IAAI;AAC3B,MAAI,UAAU,WAAW,OAAQ,MAAK,QAAQ,IAAI;AAElD,QAAM,OAAO,MAAM,SAAS,mBAAmB,SAAS,IAAI;AAC5D,aAAW,EAAE,SAAS,KAAA;AACtB,OAAK,EAAE,MAAM,kBAAkB,QAAA,CAAS;AACxC,SAAO;AACX;AAEA,eAAe,cAAc,KAA6D;AACtF,MAAI;AACA,UAAM,eAAe,IAAI,SAAS,IAAI,OAAO,IAAI,QAAQ,IAAI,EAAE;AAC/D,SAAK,EAAE,MAAM,YAAY,IAAI,IAAI,IAAI,QAAQ,SAAS;AAAA,EAC1D,SAAS,KAAK;AACV,SAAK,EAAE,MAAM,iBAAiB,IAAI,IAAI,IAAI,SAAU,KAAe,WAAW,uBAAA,CAAwB;AAAA,EAC1G;AACJ;AAEA,eAAe,eAAe,KAA8D;AACxF,QAAM,mBAAmB,IAAI,8BAAA;AAC7B,eAAa,IAAI,IAAI,IAAI,gBAAgB;AACzC,MAAI;AACA,UAAM,OAAO,MAAM,eAAe,IAAI,SAAS,IAAI,OAAO,IAAI,QAAQ,IAAI,EAAE;AAE5E,UAAM,WAAW,IAAI,aAAa,KAAK,WAAW;AAAA,MAC9C,aAAa;AAAA,MACb,qBAAqB;AAAA,MACrB,mBAAmB,CAAC,SAAiB;AACjC,YAAI,KAAM,MAAK,EAAE,MAAM,aAAa,IAAI,IAAI,IAAI,WAAW,QAAQ,OAAO,KAAA,CAAM;AAAA,MACpF;AAAA,IAAA,CACH;AAED,UAAM,cAAc,IAAI,QAAQ,eAAe;AAC/C,UAAM,KAAK,IAAI,UAAU;AAAA,MACrB,gBAAgB,IAAI,QAAQ,aAAa;AAAA,MACzC,WAAW,cAAc;AAAA,MACzB,aAAa,cAAc,IAAI,cAAc;AAAA,MAC7C;AAAA,MACA,mBAAmB;AAAA,IAAA,CACtB;AAED,SAAK,EAAE,MAAM,YAAY,IAAI,IAAI,IAAI;AAAA,EACzC,SAAS,KAAK;AACV,SAAK,EAAE,MAAM,aAAa,IAAI,IAAI,IAAI,SAAU,KAAe,WAAW,oBAAA,CAAqB;AAAA,EACnG,UAAA;AACI,iBAAa,OAAO,IAAI,EAAE;AAAA,EAC9B;AACJ;AAEA,IAAI,iBAAiB,WAAW,CAAC,UAAmC;AAChE,QAAM,MAAM,MAAM;AAClB,MAAI,IAAI,SAAS,UAAW,MAAK,cAAc,GAAG;AAAA,WACzC,IAAI,SAAS,WAAY,MAAK,eAAe,GAAG;AAAA,WAChD,IAAI,SAAS,SAAU,cAAa,IAAI,IAAI,EAAE,GAAG,UAAA;AAC9D,CAAC;"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @aparte/provider-transformers — run LLMs 100% in the browser via Transformers.js.
|
|
3
|
+
*
|
|
4
|
+
* A local, keyless `AparteAIProvider`: it owns its I/O (inference runs off the main
|
|
5
|
+
* thread in a Web Worker) so `DirectTransport` delegates to its `chat()`. Model
|
|
6
|
+
* weights download once and persist in the Cache API.
|
|
7
|
+
*
|
|
8
|
+
* Scope (v1): generic **text-generation** streaming. Tool-calling for local models is
|
|
9
|
+
* model-specific (every family has its own wire format) and is out of scope here — the
|
|
10
|
+
* app registers models and streams plain replies. Vision / embeddings can follow on demand.
|
|
11
|
+
*/
|
|
12
|
+
import type { AparteAIProvider, AparteAIModel } from '@aparte/core';
|
|
13
|
+
export interface HardwareProfile {
|
|
14
|
+
hasGpu: boolean;
|
|
15
|
+
ramGb: number;
|
|
16
|
+
tier: 'low' | 'mid' | 'high';
|
|
17
|
+
recommendedModelId: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Set the model IDs to use per hardware tier. Call before detectHardware() is used
|
|
21
|
+
* to pick a default model — the provider ships no model knowledge of its own.
|
|
22
|
+
*/
|
|
23
|
+
export declare function setHardwareTierModels(tiers: {
|
|
24
|
+
low: string;
|
|
25
|
+
mid?: string;
|
|
26
|
+
high: string;
|
|
27
|
+
}): void;
|
|
28
|
+
export declare function detectHardware(): Promise<HardwareProfile>;
|
|
29
|
+
/** Configuration for a model registered with the provider. */
|
|
30
|
+
export interface TransformersModelConfig {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
capabilities: AparteAIModel['capabilities'];
|
|
35
|
+
/** Transformers.js pipeline task — determines the model architecture / load path. */
|
|
36
|
+
task: 'text-generation';
|
|
37
|
+
/** ONNX dtype or per-part dtype map (e.g. `'q4'` or `{ decoder_model_merged: 'q4' }`). */
|
|
38
|
+
dtype?: string | Record<string, string>;
|
|
39
|
+
/** Preferred device. Defaults to WebGPU when available, else WASM. */
|
|
40
|
+
device?: 'webgpu' | 'wasm' | 'auto';
|
|
41
|
+
metadata?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Register a model with the provider. Call before the model is used for inference.
|
|
45
|
+
*/
|
|
46
|
+
export declare function registerModel(config: TransformersModelConfig): void;
|
|
47
|
+
/**
|
|
48
|
+
* Set the maximum number of models to keep in cache. When exceeded after a new
|
|
49
|
+
* model is ready, the oldest models are evicted. 0 = unlimited.
|
|
50
|
+
*/
|
|
51
|
+
export declare function setMaxCachedModels(max: number): void;
|
|
52
|
+
/** Returns the current max-cached-models setting. */
|
|
53
|
+
export declare function getMaxCachedModels(): number;
|
|
54
|
+
/**
|
|
55
|
+
* User's preferred compute backend for local inference.
|
|
56
|
+
* 'auto' → WebGPU when available, else WASM (default)
|
|
57
|
+
* 'webgpu' → force WebGPU
|
|
58
|
+
* 'wasm' → force WASM CPU
|
|
59
|
+
*/
|
|
60
|
+
export type ComputeDevice = 'auto' | 'webgpu' | 'wasm';
|
|
61
|
+
export declare function setComputeDevice(d: ComputeDevice): void;
|
|
62
|
+
export declare function getComputeDevice(): ComputeDevice;
|
|
63
|
+
export declare const TransformersProvider: AparteAIProvider;
|
|
64
|
+
export default TransformersProvider;
|
|
65
|
+
export type { AparteAIProvider, AparteAIModel, ModelStatus, ModelLoadProgress } from '@aparte/core';
|
|
66
|
+
/** Returns the modelId currently loaded in the worker's pipeline, or null. */
|
|
67
|
+
export declare function getLoadedModelId(): string | null;
|
|
68
|
+
/** Terminate the shared worker and reset in-memory state. Safe to call any time. */
|
|
69
|
+
export declare function terminateWorker(): void;
|
|
70
|
+
export interface CachedModelEntry {
|
|
71
|
+
modelId: string;
|
|
72
|
+
name: string;
|
|
73
|
+
/** Total size in bytes of all cached files for this model. -1 if unknown. */
|
|
74
|
+
sizeBytes: number;
|
|
75
|
+
/** True if the model is currently loaded in the worker. */
|
|
76
|
+
loaded: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Scan the Cache API to find which Transformers.js models have been downloaded,
|
|
80
|
+
* by matching cache entry URLs against the Hugging Face resolve path.
|
|
81
|
+
*/
|
|
82
|
+
export declare function listCachedModels(): Promise<CachedModelEntry[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Delete all cached files for a modelId from the Cache API, terminating the worker
|
|
85
|
+
* first if that model is currently loaded.
|
|
86
|
+
*/
|
|
87
|
+
export declare function deleteCachedModel(modelId: string): Promise<void>;
|
|
88
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACR,gBAAgB,EAChB,aAAa,EAMhB,MAAM,cAAc,CAAC;AAUtB,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;IAC7B,kBAAkB,EAAE,MAAM,CAAC;CAC9B;AAKD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAE9F;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,eAAe,CAAC,CA8B/D;AAMD,8DAA8D;AAC9D,MAAM,WAAW,uBAAuB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IAC5C,qFAAqF;IACrF,IAAI,EAAE,iBAAiB,CAAC;IACxB,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,sEAAsE;IACtE,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAQD;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI,CAUnE;AAaD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEpD;AAED,qDAAqD;AACrD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAGvD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI,CAEvD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,CAEhD;AAsLD,eAAO,MAAM,oBAAoB,EAAE,gBAiIlC,CAAC;AAEF,eAAe,oBAAoB,CAAC;AACpC,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAMpG,8EAA8E;AAC9E,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED,oFAAoF;AACpF,wBAAgB,eAAe,IAAI,IAAI,CAatC;AAED,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,MAAM,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAsDpE;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBtE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { contentToText } from "@aparte/core";
|
|
2
|
+
let _hardwareTiers = null;
|
|
3
|
+
function setHardwareTierModels(tiers) {
|
|
4
|
+
_hardwareTiers = tiers;
|
|
5
|
+
}
|
|
6
|
+
async function detectHardware() {
|
|
7
|
+
const ramGb = navigator.deviceMemory ?? 4;
|
|
8
|
+
let hasGpu = false;
|
|
9
|
+
if ("gpu" in navigator) {
|
|
10
|
+
try {
|
|
11
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
12
|
+
hasGpu = adapter !== null;
|
|
13
|
+
} catch {
|
|
14
|
+
hasGpu = false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
let tier;
|
|
18
|
+
if (!hasGpu || ramGb < 4) {
|
|
19
|
+
tier = "low";
|
|
20
|
+
} else if (ramGb < 8) {
|
|
21
|
+
tier = "mid";
|
|
22
|
+
} else {
|
|
23
|
+
tier = "high";
|
|
24
|
+
}
|
|
25
|
+
const recommendedModelId = _hardwareTiers ? _hardwareTiers[tier] ?? _hardwareTiers.high ?? "" : "";
|
|
26
|
+
return { hasGpu, ramGb, tier, recommendedModelId };
|
|
27
|
+
}
|
|
28
|
+
const _registeredModels = /* @__PURE__ */ new Map();
|
|
29
|
+
let _knownModels = [];
|
|
30
|
+
function registerModel(config) {
|
|
31
|
+
_registeredModels.set(config.id, config);
|
|
32
|
+
if (!_knownModels.find((m) => m.id === config.id)) {
|
|
33
|
+
_knownModels = [..._knownModels, {
|
|
34
|
+
id: config.id,
|
|
35
|
+
name: config.name,
|
|
36
|
+
description: config.description,
|
|
37
|
+
capabilities: config.capabilities
|
|
38
|
+
}];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function _modelFromCacheEntry(modelId) {
|
|
42
|
+
const config = _registeredModels.get(modelId);
|
|
43
|
+
if (config) return { id: config.id, name: config.name, description: config.description, capabilities: config.capabilities };
|
|
44
|
+
const name = (modelId.split("/").pop() ?? modelId).replace(/-/g, " ");
|
|
45
|
+
return { id: modelId, name, capabilities: ["streaming"] };
|
|
46
|
+
}
|
|
47
|
+
let _maxCachedModels = 1;
|
|
48
|
+
function setMaxCachedModels(max) {
|
|
49
|
+
_maxCachedModels = max;
|
|
50
|
+
}
|
|
51
|
+
function getMaxCachedModels() {
|
|
52
|
+
return _maxCachedModels;
|
|
53
|
+
}
|
|
54
|
+
let _computeDevice = "auto";
|
|
55
|
+
function setComputeDevice(d) {
|
|
56
|
+
_computeDevice = d;
|
|
57
|
+
}
|
|
58
|
+
function getComputeDevice() {
|
|
59
|
+
return _computeDevice;
|
|
60
|
+
}
|
|
61
|
+
async function _enforceMaxCachedModels(keepModelId) {
|
|
62
|
+
if (_maxCachedModels === 0) return;
|
|
63
|
+
try {
|
|
64
|
+
const cached = await listCachedModels();
|
|
65
|
+
const others = cached.filter((e) => e.modelId !== keepModelId);
|
|
66
|
+
const excess = cached.length - _maxCachedModels;
|
|
67
|
+
if (excess <= 0) return;
|
|
68
|
+
for (let i = 0; i < excess && i < others.length; i++) {
|
|
69
|
+
await deleteCachedModel(others[i].modelId);
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function _refreshKnownModels() {
|
|
75
|
+
try {
|
|
76
|
+
const cached = await listCachedModels();
|
|
77
|
+
for (const entry of cached) {
|
|
78
|
+
if (!_knownModels.find((m) => m.id === entry.modelId)) {
|
|
79
|
+
_knownModels = [..._knownModels, _modelFromCacheEntry(entry.modelId)];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function toMessages(messages) {
|
|
86
|
+
const result = [];
|
|
87
|
+
for (const m of messages) {
|
|
88
|
+
if (m.role === "user" || m.role === "assistant" || m.role === "system") {
|
|
89
|
+
const text = contentToText(m.content);
|
|
90
|
+
if (text) result.push({ role: m.role, content: text });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
let _worker = null;
|
|
96
|
+
const _pendingPrepares = /* @__PURE__ */ new Map();
|
|
97
|
+
const _pendingGenerates = /* @__PURE__ */ new Map();
|
|
98
|
+
let _generateChain = Promise.resolve();
|
|
99
|
+
const _generateDoneResolvers = /* @__PURE__ */ new Map();
|
|
100
|
+
function _releaseGenerateSlot(id) {
|
|
101
|
+
const resolve = _generateDoneResolvers.get(id);
|
|
102
|
+
if (resolve) {
|
|
103
|
+
_generateDoneResolvers.delete(id);
|
|
104
|
+
resolve();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let _loadedModelId = null;
|
|
108
|
+
let _preparingModelId = null;
|
|
109
|
+
function _getWorker() {
|
|
110
|
+
if (!_worker) {
|
|
111
|
+
_worker = new Worker(new URL(
|
|
112
|
+
/* @vite-ignore */
|
|
113
|
+
"" + new URL("assets/worker-Bk-8pt3W.js", import.meta.url).href,
|
|
114
|
+
import.meta.url
|
|
115
|
+
), { type: "module" });
|
|
116
|
+
_worker.addEventListener("message", _handleWorkerMessage);
|
|
117
|
+
_worker.addEventListener("error", _handleWorkerError);
|
|
118
|
+
_worker.addEventListener("messageerror", _handleWorkerError);
|
|
119
|
+
}
|
|
120
|
+
return _worker;
|
|
121
|
+
}
|
|
122
|
+
function _handleWorkerError(e) {
|
|
123
|
+
const message = e?.message || "Worker crashed unexpectedly";
|
|
124
|
+
for (const p of _pendingPrepares.values()) {
|
|
125
|
+
try {
|
|
126
|
+
p.reject(new Error(message));
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
_pendingPrepares.clear();
|
|
131
|
+
for (const ctrl of _pendingGenerates.values()) {
|
|
132
|
+
try {
|
|
133
|
+
ctrl.enqueue({ type: "error", message });
|
|
134
|
+
ctrl.close();
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
_pendingGenerates.clear();
|
|
139
|
+
for (const resolve of _generateDoneResolvers.values()) {
|
|
140
|
+
try {
|
|
141
|
+
resolve();
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
_generateDoneResolvers.clear();
|
|
146
|
+
_generateChain = Promise.resolve();
|
|
147
|
+
_loadedModelId = null;
|
|
148
|
+
_preparingModelId = null;
|
|
149
|
+
try {
|
|
150
|
+
_worker?.terminate();
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
_worker = null;
|
|
154
|
+
}
|
|
155
|
+
function _handleWorkerMessage(event) {
|
|
156
|
+
const msg = event.data;
|
|
157
|
+
switch (msg.type) {
|
|
158
|
+
case "progress": {
|
|
159
|
+
const pending = _pendingPrepares.get(msg.id);
|
|
160
|
+
if (!pending) break;
|
|
161
|
+
if (msg.status === "ready") {
|
|
162
|
+
pending.onProgress({ status: "ready" });
|
|
163
|
+
pending.resolve();
|
|
164
|
+
_pendingPrepares.delete(msg.id);
|
|
165
|
+
} else if (msg.status === "loading") {
|
|
166
|
+
pending.onProgress({ status: "loading" });
|
|
167
|
+
} else if (msg.status === "cached") {
|
|
168
|
+
pending.onProgress({ status: "cached", file: msg.file, progress: msg.progress });
|
|
169
|
+
} else {
|
|
170
|
+
pending.onProgress({ status: "downloading", file: msg.file, progress: msg.progress });
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case "prepare-error": {
|
|
175
|
+
const pending = _pendingPrepares.get(msg.id);
|
|
176
|
+
if (!pending) break;
|
|
177
|
+
pending.reject(new Error(msg.message));
|
|
178
|
+
_pendingPrepares.delete(msg.id);
|
|
179
|
+
if (_preparingModelId === pending.modelId) _preparingModelId = null;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case "pipeline-ready": {
|
|
183
|
+
_loadedModelId = msg.modelId;
|
|
184
|
+
_preparingModelId = null;
|
|
185
|
+
void _enforceMaxCachedModels(msg.modelId).then(() => _refreshKnownModels());
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case "gen-chunk": {
|
|
189
|
+
const ctrl = _pendingGenerates.get(msg.id);
|
|
190
|
+
if (!ctrl) break;
|
|
191
|
+
ctrl.enqueue({ type: msg.chunkType, delta: msg.delta });
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "gen-done": {
|
|
195
|
+
_releaseGenerateSlot(msg.id);
|
|
196
|
+
const ctrl = _pendingGenerates.get(msg.id);
|
|
197
|
+
if (!ctrl) break;
|
|
198
|
+
ctrl.enqueue({ type: "done", ...msg.usage ? { usage: msg.usage } : {} });
|
|
199
|
+
ctrl.close();
|
|
200
|
+
_pendingGenerates.delete(msg.id);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case "gen-error": {
|
|
204
|
+
_releaseGenerateSlot(msg.id);
|
|
205
|
+
const ctrl = _pendingGenerates.get(msg.id);
|
|
206
|
+
if (!ctrl) break;
|
|
207
|
+
ctrl.enqueue({ type: "error", message: msg.message });
|
|
208
|
+
ctrl.close();
|
|
209
|
+
_pendingGenerates.delete(msg.id);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const TransformersProvider = {
|
|
215
|
+
id: "transformers",
|
|
216
|
+
getMetadata() {
|
|
217
|
+
return {
|
|
218
|
+
id: "transformers",
|
|
219
|
+
name: "Transformers.js",
|
|
220
|
+
icon: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2L2 7l10 5 10-5-10-5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 17l10 5 10-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 12l10 5 10-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
|
221
|
+
color: "#f59e0b",
|
|
222
|
+
description: "Run LLMs directly in your browser via WebGPU or WASM — no API, no key",
|
|
223
|
+
hasFreeModels: true,
|
|
224
|
+
isLocal: true,
|
|
225
|
+
helpUrl: "https://huggingface.co/docs/transformers.js"
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
getModels() {
|
|
229
|
+
return _knownModels;
|
|
230
|
+
},
|
|
231
|
+
async fetchModels() {
|
|
232
|
+
await _refreshKnownModels();
|
|
233
|
+
return _knownModels;
|
|
234
|
+
},
|
|
235
|
+
async chat(request) {
|
|
236
|
+
const messages = toMessages(request.messages);
|
|
237
|
+
const requestId = crypto.randomUUID();
|
|
238
|
+
const options = {
|
|
239
|
+
maxTokens: request.maxTokens,
|
|
240
|
+
temperature: request.temperature,
|
|
241
|
+
seed: request.seed
|
|
242
|
+
};
|
|
243
|
+
const task = _registeredModels.get(request.modelId)?.task ?? "text-generation";
|
|
244
|
+
const prevGenerate = _generateChain;
|
|
245
|
+
_generateChain = new Promise((resolveSlot) => {
|
|
246
|
+
_generateDoneResolvers.set(requestId, resolveSlot);
|
|
247
|
+
});
|
|
248
|
+
const postGenerate = () => {
|
|
249
|
+
_getWorker().postMessage({
|
|
250
|
+
type: "generate",
|
|
251
|
+
id: requestId,
|
|
252
|
+
modelId: request.modelId,
|
|
253
|
+
messages,
|
|
254
|
+
options,
|
|
255
|
+
task,
|
|
256
|
+
dtype: _registeredModels.get(request.modelId)?.dtype,
|
|
257
|
+
device: _computeDevice
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
if (request.stream === false) {
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
let result = "";
|
|
263
|
+
const fakeCtrl = {
|
|
264
|
+
enqueue: (chunk) => {
|
|
265
|
+
if (chunk.type === "text") result += chunk.delta ?? "";
|
|
266
|
+
else if (chunk.type === "done") resolve(result);
|
|
267
|
+
else if (chunk.type === "error") reject(new Error(chunk.message));
|
|
268
|
+
},
|
|
269
|
+
close: () => {
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
_pendingGenerates.set(requestId, fakeCtrl);
|
|
273
|
+
void prevGenerate.then(postGenerate);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return new ReadableStream({
|
|
277
|
+
async start(controller) {
|
|
278
|
+
_pendingGenerates.set(requestId, controller);
|
|
279
|
+
await prevGenerate;
|
|
280
|
+
postGenerate();
|
|
281
|
+
},
|
|
282
|
+
cancel() {
|
|
283
|
+
_pendingGenerates.delete(requestId);
|
|
284
|
+
_getWorker().postMessage({ type: "cancel", id: requestId });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
async getModelStatus(modelId) {
|
|
289
|
+
if (_loadedModelId === modelId) return "ready";
|
|
290
|
+
if (_preparingModelId === modelId) return "cached";
|
|
291
|
+
if ("caches" in globalThis) {
|
|
292
|
+
try {
|
|
293
|
+
const encodedId = encodeURIComponent(modelId);
|
|
294
|
+
const names = await caches.keys();
|
|
295
|
+
for (const name of names) {
|
|
296
|
+
const cache = await caches.open(name);
|
|
297
|
+
const keys = await cache.keys();
|
|
298
|
+
if (keys.some((r) => r.url.includes(encodedId) || r.url.includes(modelId + "/"))) {
|
|
299
|
+
return "cached";
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return "not-downloaded";
|
|
306
|
+
},
|
|
307
|
+
async prepareModel(modelId, onProgress) {
|
|
308
|
+
if (_loadedModelId === modelId) {
|
|
309
|
+
onProgress({ status: "ready" });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const requestId = crypto.randomUUID();
|
|
313
|
+
_preparingModelId = modelId;
|
|
314
|
+
const task = _registeredModels.get(modelId)?.task ?? "text-generation";
|
|
315
|
+
const dtype = _registeredModels.get(modelId)?.dtype;
|
|
316
|
+
return new Promise((resolve, reject) => {
|
|
317
|
+
_pendingPrepares.set(requestId, { modelId, onProgress, resolve, reject });
|
|
318
|
+
_getWorker().postMessage({ type: "prepare", id: requestId, modelId, task, dtype, device: _computeDevice });
|
|
319
|
+
});
|
|
320
|
+
},
|
|
321
|
+
async deleteModel(modelId) {
|
|
322
|
+
await deleteCachedModel(modelId);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
function getLoadedModelId() {
|
|
326
|
+
return _loadedModelId;
|
|
327
|
+
}
|
|
328
|
+
function terminateWorker() {
|
|
329
|
+
_worker?.terminate();
|
|
330
|
+
_worker = null;
|
|
331
|
+
_loadedModelId = null;
|
|
332
|
+
_preparingModelId = null;
|
|
333
|
+
for (const [, p] of _pendingPrepares) {
|
|
334
|
+
p.reject(new Error("Worker terminated"));
|
|
335
|
+
}
|
|
336
|
+
_pendingPrepares.clear();
|
|
337
|
+
for (const [, ctrl] of _pendingGenerates) {
|
|
338
|
+
try {
|
|
339
|
+
ctrl.enqueue({ type: "error", message: "Worker terminated" });
|
|
340
|
+
ctrl.close();
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
_pendingGenerates.clear();
|
|
345
|
+
}
|
|
346
|
+
async function listCachedModels() {
|
|
347
|
+
if (!("caches" in globalThis)) return [];
|
|
348
|
+
const found = /* @__PURE__ */ new Map();
|
|
349
|
+
function extractModelId(url) {
|
|
350
|
+
const m = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\//);
|
|
351
|
+
return m ? decodeURIComponent(m[1]) : null;
|
|
352
|
+
}
|
|
353
|
+
function modelName(modelId) {
|
|
354
|
+
const config = _registeredModels.get(modelId);
|
|
355
|
+
if (config) return config.name;
|
|
356
|
+
return (modelId.split("/").pop() ?? modelId).replace(/-/g, " ");
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
const cacheNames = await caches.keys();
|
|
360
|
+
await Promise.all(cacheNames.map(async (cacheName) => {
|
|
361
|
+
try {
|
|
362
|
+
const cache = await caches.open(cacheName);
|
|
363
|
+
const requests = await cache.keys();
|
|
364
|
+
for (const req of requests) {
|
|
365
|
+
const modelId = extractModelId(req.url);
|
|
366
|
+
if (!modelId) continue;
|
|
367
|
+
if (!found.has(modelId)) {
|
|
368
|
+
found.set(modelId, { name: modelName(modelId), sizeBytes: 0 });
|
|
369
|
+
}
|
|
370
|
+
const response = await cache.match(req);
|
|
371
|
+
if (!response) continue;
|
|
372
|
+
const contentLength = response.headers.get("content-length");
|
|
373
|
+
if (contentLength) {
|
|
374
|
+
found.get(modelId).sizeBytes += parseInt(contentLength, 10);
|
|
375
|
+
} else {
|
|
376
|
+
try {
|
|
377
|
+
const blob = await response.clone().blob();
|
|
378
|
+
found.get(modelId).sizeBytes += blob.size;
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
}));
|
|
386
|
+
} catch {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
return Array.from(found.entries()).map(([modelId, { name, sizeBytes }]) => ({
|
|
390
|
+
modelId,
|
|
391
|
+
name,
|
|
392
|
+
sizeBytes,
|
|
393
|
+
loaded: _loadedModelId === modelId
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
async function deleteCachedModel(modelId) {
|
|
397
|
+
if (_loadedModelId === modelId || _preparingModelId === modelId) {
|
|
398
|
+
terminateWorker();
|
|
399
|
+
}
|
|
400
|
+
if (!("caches" in globalThis)) return;
|
|
401
|
+
try {
|
|
402
|
+
const cacheNames = await caches.keys();
|
|
403
|
+
await Promise.all(cacheNames.map(async (cacheName) => {
|
|
404
|
+
try {
|
|
405
|
+
const cache = await caches.open(cacheName);
|
|
406
|
+
const requests = await cache.keys();
|
|
407
|
+
const encoded = encodeURIComponent(modelId);
|
|
408
|
+
await Promise.all(
|
|
409
|
+
requests.filter((r) => r.url.includes(modelId) || r.url.includes(encoded)).map((r) => cache.delete(r))
|
|
410
|
+
);
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
413
|
+
}));
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
export {
|
|
418
|
+
TransformersProvider,
|
|
419
|
+
TransformersProvider as default,
|
|
420
|
+
deleteCachedModel,
|
|
421
|
+
detectHardware,
|
|
422
|
+
getComputeDevice,
|
|
423
|
+
getLoadedModelId,
|
|
424
|
+
getMaxCachedModels,
|
|
425
|
+
listCachedModels,
|
|
426
|
+
registerModel,
|
|
427
|
+
setComputeDevice,
|
|
428
|
+
setHardwareTierModels,
|
|
429
|
+
setMaxCachedModels,
|
|
430
|
+
terminateWorker
|
|
431
|
+
};
|
|
432
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * @aparte/provider-transformers — run LLMs 100% in the browser via Transformers.js.\n *\n * A local, keyless `AparteAIProvider`: it owns its I/O (inference runs off the main\n * thread in a Web Worker) so `DirectTransport` delegates to its `chat()`. Model\n * weights download once and persist in the Cache API.\n *\n * Scope (v1): generic **text-generation** streaming. Tool-calling for local models is\n * model-specific (every family has its own wire format) and is out of scope here — the\n * app registers models and streams plain replies. Vision / embeddings can follow on demand.\n */\n\nimport type {\n AparteAIProvider,\n AparteAIModel,\n AparteChatRequest,\n AparteChatResponse,\n AparteChatMessage,\n ModelStatus,\n ModelLoadProgress,\n} from '@aparte/core';\nimport { contentToText } from '@aparte/core';\n\n/** The minimal chat shape passed to the worker (the tokenizer applies the chat template). */\ntype SimpleMessage = { role: 'user' | 'assistant' | 'system'; content: string };\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Hardware detection\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface HardwareProfile {\n hasGpu: boolean;\n ramGb: number;\n tier: 'low' | 'mid' | 'high';\n recommendedModelId: string;\n}\n\n/** Hardware-tier model overrides — set by the app via setHardwareTierModels(). */\nlet _hardwareTiers: { low: string; mid?: string; high: string } | null = null;\n\n/**\n * Set the model IDs to use per hardware tier. Call before detectHardware() is used\n * to pick a default model — the provider ships no model knowledge of its own.\n */\nexport function setHardwareTierModels(tiers: { low: string; mid?: string; high: string }): void {\n _hardwareTiers = tiers;\n}\n\nexport async function detectHardware(): Promise<HardwareProfile> {\n // navigator.deviceMemory: W3C API, Chromium only, capped at 8 GB for privacy\n // (1 | 2 | 4 | 8). Falls back to 4 on Firefox/Safari.\n const ramGb: number = (navigator as unknown as { deviceMemory?: number }).deviceMemory ?? 4;\n\n // Real WebGPU check: requestAdapter() returns null if no capable GPU is present.\n let hasGpu = false;\n if ('gpu' in navigator) {\n try {\n const adapter = await (navigator as unknown as { gpu: { requestAdapter(): Promise<unknown> } }).gpu.requestAdapter();\n hasGpu = adapter !== null;\n } catch {\n hasGpu = false;\n }\n }\n\n let tier: 'low' | 'mid' | 'high';\n if (!hasGpu || ramGb < 4) {\n tier = 'low';\n } else if (ramGb < 8) {\n tier = 'mid';\n } else {\n tier = 'high';\n }\n\n const recommendedModelId = _hardwareTiers\n ? (_hardwareTiers[tier] ?? _hardwareTiers.high ?? '')\n : '';\n\n return { hasGpu, ramGb, tier, recommendedModelId };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Model catalog — all model knowledge lives in the app, not the provider.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Configuration for a model registered with the provider. */\nexport interface TransformersModelConfig {\n id: string;\n name: string;\n description?: string;\n capabilities: AparteAIModel['capabilities'];\n /** Transformers.js pipeline task — determines the model architecture / load path. */\n task: 'text-generation';\n /** ONNX dtype or per-part dtype map (e.g. `'q4'` or `{ decoder_model_merged: 'q4' }`). */\n dtype?: string | Record<string, string>;\n /** Preferred device. Defaults to WebGPU when available, else WASM. */\n device?: 'webgpu' | 'wasm' | 'auto';\n metadata?: Record<string, unknown>;\n}\n\n/** Models registered by the app via registerModel(). */\nconst _registeredModels = new Map<string, TransformersModelConfig>();\n\n/** Mutable model list — populated by registerModel() and cache discovery. */\nlet _knownModels: AparteAIModel[] = [];\n\n/**\n * Register a model with the provider. Call before the model is used for inference.\n */\nexport function registerModel(config: TransformersModelConfig): void {\n _registeredModels.set(config.id, config);\n if (!_knownModels.find(m => m.id === config.id)) {\n _knownModels = [..._knownModels, {\n id: config.id,\n name: config.name,\n description: config.description,\n capabilities: config.capabilities,\n }];\n }\n}\n\n/** Build an AparteAIModel entry from a cache-discovered modelId not in the registry. */\nfunction _modelFromCacheEntry(modelId: string): AparteAIModel {\n const config = _registeredModels.get(modelId);\n if (config) return { id: config.id, name: config.name, description: config.description, capabilities: config.capabilities };\n const name = (modelId.split('/').pop() ?? modelId).replace(/-/g, ' ');\n return { id: modelId, name, capabilities: ['streaming'] };\n}\n\n/** Max number of models to keep in cache. 0 = unlimited. Default: 1. */\nlet _maxCachedModels = 1;\n\n/**\n * Set the maximum number of models to keep in cache. When exceeded after a new\n * model is ready, the oldest models are evicted. 0 = unlimited.\n */\nexport function setMaxCachedModels(max: number): void {\n _maxCachedModels = max;\n}\n\n/** Returns the current max-cached-models setting. */\nexport function getMaxCachedModels(): number {\n return _maxCachedModels;\n}\n\n/**\n * User's preferred compute backend for local inference.\n * 'auto' → WebGPU when available, else WASM (default)\n * 'webgpu' → force WebGPU\n * 'wasm' → force WASM CPU\n */\nexport type ComputeDevice = 'auto' | 'webgpu' | 'wasm';\nlet _computeDevice: ComputeDevice = 'auto';\n\nexport function setComputeDevice(d: ComputeDevice): void {\n _computeDevice = d;\n}\n\nexport function getComputeDevice(): ComputeDevice {\n return _computeDevice;\n}\n\n/** Evict models from cache until count <= _maxCachedModels; `keepModelId` is never evicted. */\nasync function _enforceMaxCachedModels(keepModelId: string): Promise<void> {\n if (_maxCachedModels === 0) return; // unlimited\n try {\n const cached = await listCachedModels();\n const others = cached.filter(e => e.modelId !== keepModelId);\n const excess = cached.length - _maxCachedModels;\n if (excess <= 0) return;\n // Delete the excess models (oldest first — they appear first in cache scan order).\n for (let i = 0; i < excess && i < others.length; i++) {\n await deleteCachedModel(others[i]!.modelId);\n }\n } catch { /* cache unavailable */ }\n}\n\n/** Merge cached models into _knownModels (idempotent). Called by fetchModels(). */\nasync function _refreshKnownModels(): Promise<void> {\n try {\n const cached = await listCachedModels();\n for (const entry of cached) {\n if (!_knownModels.find(m => m.id === entry.modelId)) {\n _knownModels = [..._knownModels, _modelFromCacheEntry(entry.modelId)];\n }\n }\n } catch { /* cache unavailable */ }\n}\n\n/** AparteChatMessage[] → plain chat turns (the tokenizer's chat template does the rest). */\nfunction toMessages(messages: AparteChatMessage[]): SimpleMessage[] {\n const result: SimpleMessage[] = [];\n for (const m of messages) {\n if (m.role === 'user' || m.role === 'assistant' || m.role === 'system') {\n const text = contentToText(m.content);\n if (text) result.push({ role: m.role, content: text });\n }\n // tool_call / tool_result are not supported by this generic provider (v1).\n }\n return result;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Worker bridge\n// ─────────────────────────────────────────────────────────────────────────────\n\nlet _worker: Worker | null = null;\n\ninterface PendingPrepare {\n modelId: string;\n onProgress: (p: ModelLoadProgress) => void;\n resolve: () => void;\n reject: (err: Error) => void;\n}\nconst _pendingPrepares = new Map<string, PendingPrepare>();\nconst _pendingGenerates = new Map<string, ReadableStreamDefaultController>();\n\n// ── Generate serialization ──────────────────────────────────────────────────\n// The worker holds ONE pipeline: two concurrent generates would corrupt each\n// other. Each chat() chains its `generate` behind the previous generate's\n// completion (gen-done / gen-error).\nlet _generateChain: Promise<void> = Promise.resolve();\nconst _generateDoneResolvers = new Map<string, () => void>();\n\n/** Settle the serialization slot for a finished generate. */\nfunction _releaseGenerateSlot(id: string): void {\n const resolve = _generateDoneResolvers.get(id);\n if (resolve) {\n _generateDoneResolvers.delete(id);\n resolve();\n }\n}\n\n/** Model known to be loaded (main-thread view). */\nlet _loadedModelId: string | null = null;\n/** Model currently being prepared (for the getModelStatus 'cached' path). */\nlet _preparingModelId: string | null = null;\n\nfunction _getWorker(): Worker {\n if (!_worker) {\n _worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });\n _worker.addEventListener('message', _handleWorkerMessage);\n _worker.addEventListener('error', _handleWorkerError);\n _worker.addEventListener('messageerror', _handleWorkerError);\n }\n return _worker;\n}\n\n/**\n * Worker crashed (uncaught error / WASM init failure / OOM). Reject every in-flight\n * prepare and close every open generate stream so the UI doesn't hang. Subsequent\n * calls rebuild the worker.\n */\nfunction _handleWorkerError(e: Event): void {\n const message = (e as ErrorEvent)?.message || 'Worker crashed unexpectedly';\n\n for (const p of _pendingPrepares.values()) {\n try { p.reject(new Error(message)); } catch { /* ignore */ }\n }\n _pendingPrepares.clear();\n\n for (const ctrl of _pendingGenerates.values()) {\n try { ctrl.enqueue({ type: 'error' as const, message }); ctrl.close(); }\n catch { /* ignore */ }\n }\n _pendingGenerates.clear();\n\n // Release every serialization slot so the generate chain doesn't deadlock.\n for (const resolve of _generateDoneResolvers.values()) {\n try { resolve(); } catch { /* ignore */ }\n }\n _generateDoneResolvers.clear();\n _generateChain = Promise.resolve();\n\n _loadedModelId = null;\n _preparingModelId = null;\n try { _worker?.terminate(); } catch { /* ignore */ }\n _worker = null;\n}\n\nfunction _handleWorkerMessage(event: MessageEvent): void {\n const msg = event.data;\n\n switch (msg.type) {\n case 'progress': {\n const pending = _pendingPrepares.get(msg.id);\n if (!pending) break;\n if (msg.status === 'ready') {\n pending.onProgress({ status: 'ready' });\n pending.resolve();\n _pendingPrepares.delete(msg.id);\n } else if (msg.status === 'loading') {\n pending.onProgress({ status: 'loading' });\n } else if (msg.status === 'cached') {\n pending.onProgress({ status: 'cached', file: msg.file, progress: msg.progress });\n } else {\n pending.onProgress({ status: 'downloading', file: msg.file, progress: msg.progress });\n }\n break;\n }\n case 'prepare-error': {\n const pending = _pendingPrepares.get(msg.id);\n if (!pending) break;\n pending.reject(new Error(msg.message));\n _pendingPrepares.delete(msg.id);\n if (_preparingModelId === pending.modelId) _preparingModelId = null;\n break;\n }\n case 'pipeline-ready': {\n _loadedModelId = msg.modelId;\n _preparingModelId = null;\n // Evict models over the cache limit, then refresh the known list.\n void _enforceMaxCachedModels(msg.modelId).then(() => _refreshKnownModels());\n break;\n }\n case 'gen-chunk': {\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue({ type: msg.chunkType as 'text' | 'thinking', delta: msg.delta });\n break;\n }\n case 'gen-done': {\n _releaseGenerateSlot(msg.id);\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue({ type: 'done' as const, ...(msg.usage ? { usage: msg.usage } : {}) });\n ctrl.close();\n _pendingGenerates.delete(msg.id);\n break;\n }\n case 'gen-error': {\n _releaseGenerateSlot(msg.id);\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue({ type: 'error' as const, message: msg.message });\n ctrl.close();\n _pendingGenerates.delete(msg.id);\n break;\n }\n }\n}\n\nexport const TransformersProvider: AparteAIProvider = {\n id: 'transformers',\n\n getMetadata() {\n return {\n id: 'transformers',\n name: 'Transformers.js',\n icon: `<svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12 2L2 7l10 5 10-5-10-5z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M2 17l10 5 10-5\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M2 12l10 5 10-5\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`,\n color: '#f59e0b',\n description: 'Run LLMs directly in your browser via WebGPU or WASM — no API, no key',\n hasFreeModels: true,\n isLocal: true,\n helpUrl: 'https://huggingface.co/docs/transformers.js',\n };\n },\n\n getModels(): AparteAIModel[] {\n return _knownModels;\n },\n\n async fetchModels(): Promise<AparteAIModel[]> {\n await _refreshKnownModels();\n return _knownModels;\n },\n\n async chat(request: AparteChatRequest): Promise<AparteChatResponse> {\n const messages = toMessages(request.messages);\n const requestId = crypto.randomUUID();\n const options = {\n maxTokens: request.maxTokens,\n temperature: request.temperature,\n seed: request.seed,\n };\n const task = _registeredModels.get(request.modelId)?.task ?? 'text-generation';\n\n // ── Reserve a serialization slot ─────────────────────────────────────\n // Chain this generate behind the previous one; the worker has a single\n // pipeline, so generates MUST NOT overlap.\n const prevGenerate = _generateChain;\n _generateChain = new Promise<void>((resolveSlot) => {\n _generateDoneResolvers.set(requestId, resolveSlot);\n });\n const postGenerate = (): void => {\n _getWorker().postMessage({\n type: 'generate',\n id: requestId,\n modelId: request.modelId,\n messages,\n options,\n task,\n dtype: _registeredModels.get(request.modelId)?.dtype,\n device: _computeDevice,\n });\n };\n\n if (request.stream === false) {\n return new Promise<string>((resolve, reject) => {\n let result = '';\n const fakeCtrl = {\n enqueue: (chunk: { type: string; delta?: string; message?: string }) => {\n if (chunk.type === 'text') result += chunk.delta ?? '';\n else if (chunk.type === 'done') resolve(result);\n else if (chunk.type === 'error') reject(new Error(chunk.message));\n },\n close: () => { /* no-op */ },\n } as unknown as ReadableStreamDefaultController;\n _pendingGenerates.set(requestId, fakeCtrl);\n void prevGenerate.then(postGenerate);\n });\n }\n\n return new ReadableStream({\n async start(controller) {\n _pendingGenerates.set(requestId, controller);\n await prevGenerate;\n postGenerate();\n },\n cancel() {\n _pendingGenerates.delete(requestId);\n // Actually STOP the model (not just detach the reader): tell the worker\n // to interrupt this generate. The serialization slot is still released\n // by the resulting gen-done/gen-error, so a queued generate can't start\n // before the worker has stopped this one.\n _getWorker().postMessage({ type: 'cancel', id: requestId });\n },\n });\n },\n\n async getModelStatus(modelId: string): Promise<ModelStatus> {\n if (_loadedModelId === modelId) return 'ready';\n if (_preparingModelId === modelId) return 'cached';\n if ('caches' in globalThis) {\n try {\n const encodedId = encodeURIComponent(modelId);\n const names = await caches.keys();\n for (const name of names) {\n const cache = await caches.open(name);\n const keys = await cache.keys();\n if (keys.some(r => r.url.includes(encodedId) || r.url.includes(modelId + '/'))) {\n return 'cached';\n }\n }\n } catch {\n // Cache API unavailable\n }\n }\n return 'not-downloaded';\n },\n\n async prepareModel(modelId: string, onProgress: (p: ModelLoadProgress) => void): Promise<void> {\n if (_loadedModelId === modelId) {\n onProgress({ status: 'ready' });\n return;\n }\n\n const requestId = crypto.randomUUID();\n _preparingModelId = modelId;\n\n const task = _registeredModels.get(modelId)?.task ?? 'text-generation';\n const dtype = _registeredModels.get(modelId)?.dtype;\n return new Promise<void>((resolve, reject) => {\n _pendingPrepares.set(requestId, { modelId, onProgress, resolve, reject });\n _getWorker().postMessage({ type: 'prepare', id: requestId, modelId, task, dtype, device: _computeDevice });\n });\n },\n\n async deleteModel(modelId: string): Promise<void> {\n await deleteCachedModel(modelId);\n },\n};\n\nexport default TransformersProvider;\nexport type { AparteAIProvider, AparteAIModel, ModelStatus, ModelLoadProgress } from '@aparte/core';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cache utilities (settings panels, etc.)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Returns the modelId currently loaded in the worker's pipeline, or null. */\nexport function getLoadedModelId(): string | null {\n return _loadedModelId;\n}\n\n/** Terminate the shared worker and reset in-memory state. Safe to call any time. */\nexport function terminateWorker(): void {\n _worker?.terminate();\n _worker = null;\n _loadedModelId = null;\n _preparingModelId = null;\n for (const [, p] of _pendingPrepares) {\n p.reject(new Error('Worker terminated'));\n }\n _pendingPrepares.clear();\n for (const [, ctrl] of _pendingGenerates) {\n try { ctrl.enqueue({ type: 'error' as const, message: 'Worker terminated' }); ctrl.close(); } catch { /* already closed */ }\n }\n _pendingGenerates.clear();\n}\n\nexport interface CachedModelEntry {\n modelId: string;\n name: string;\n /** Total size in bytes of all cached files for this model. -1 if unknown. */\n sizeBytes: number;\n /** True if the model is currently loaded in the worker. */\n loaded: boolean;\n}\n\n/**\n * Scan the Cache API to find which Transformers.js models have been downloaded,\n * by matching cache entry URLs against the Hugging Face resolve path.\n */\nexport async function listCachedModels(): Promise<CachedModelEntry[]> {\n if (!('caches' in globalThis)) return [];\n\n const found = new Map<string, { name: string; sizeBytes: number }>();\n\n // e.g. https://huggingface.co/onnx-community/Qwen2.5-0.5B/resolve/main/config.json\n // → onnx-community/Qwen2.5-0.5B\n function extractModelId(url: string): string | null {\n const m = url.match(/huggingface\\.co\\/([^/]+\\/[^/]+)\\/resolve\\//);\n return m ? decodeURIComponent(m[1]!) : null;\n }\n\n function modelName(modelId: string): string {\n const config = _registeredModels.get(modelId);\n if (config) return config.name;\n return (modelId.split('/').pop() ?? modelId).replace(/-/g, ' ');\n }\n\n try {\n const cacheNames = await caches.keys();\n await Promise.all(cacheNames.map(async (cacheName) => {\n try {\n const cache = await caches.open(cacheName);\n const requests = await cache.keys();\n for (const req of requests) {\n const modelId = extractModelId(req.url);\n if (!modelId) continue;\n if (!found.has(modelId)) {\n found.set(modelId, { name: modelName(modelId), sizeBytes: 0 });\n }\n const response = await cache.match(req);\n if (!response) continue;\n const contentLength = response.headers.get('content-length');\n if (contentLength) {\n found.get(modelId)!.sizeBytes += parseInt(contentLength, 10);\n } else {\n try {\n const blob = await response.clone().blob();\n found.get(modelId)!.sizeBytes += blob.size;\n } catch { /* skip */ }\n }\n }\n } catch { /* skip inaccessible cache */ }\n }));\n } catch {\n return [];\n }\n\n return Array.from(found.entries()).map(([modelId, { name, sizeBytes }]) => ({\n modelId,\n name,\n sizeBytes,\n loaded: _loadedModelId === modelId,\n }));\n}\n\n/**\n * Delete all cached files for a modelId from the Cache API, terminating the worker\n * first if that model is currently loaded.\n */\nexport async function deleteCachedModel(modelId: string): Promise<void> {\n if (_loadedModelId === modelId || _preparingModelId === modelId) {\n terminateWorker();\n }\n if (!('caches' in globalThis)) return;\n try {\n const cacheNames = await caches.keys();\n await Promise.all(cacheNames.map(async (cacheName) => {\n try {\n const cache = await caches.open(cacheName);\n const requests = await cache.keys();\n const encoded = encodeURIComponent(modelId);\n await Promise.all(\n requests\n .filter(r => r.url.includes(modelId) || r.url.includes(encoded))\n .map(r => cache.delete(r)),\n );\n } catch { /* skip */ }\n }));\n } catch { /* Cache API unavailable */ }\n}\n"],"names":[],"mappings":";AAsCA,IAAI,iBAAqE;AAMlE,SAAS,sBAAsB,OAA0D;AAC5F,mBAAiB;AACrB;AAEA,eAAsB,iBAA2C;AAG7D,QAAM,QAAiB,UAAmD,gBAAgB;AAG1F,MAAI,SAAS;AACb,MAAI,SAAS,WAAW;AACpB,QAAI;AACA,YAAM,UAAU,MAAO,UAAyE,IAAI,eAAA;AACpG,eAAS,YAAY;AAAA,IACzB,QAAQ;AACJ,eAAS;AAAA,IACb;AAAA,EACJ;AAEA,MAAI;AACJ,MAAI,CAAC,UAAU,QAAQ,GAAG;AACtB,WAAO;AAAA,EACX,WAAW,QAAQ,GAAG;AAClB,WAAO;AAAA,EACX,OAAO;AACH,WAAO;AAAA,EACX;AAEA,QAAM,qBAAqB,iBACpB,eAAe,IAAI,KAAK,eAAe,QAAQ,KAChD;AAEN,SAAO,EAAE,QAAQ,OAAO,MAAM,mBAAA;AAClC;AAsBA,MAAM,wCAAwB,IAAA;AAG9B,IAAI,eAAgC,CAAA;AAK7B,SAAS,cAAc,QAAuC;AACjE,oBAAkB,IAAI,OAAO,IAAI,MAAM;AACvC,MAAI,CAAC,aAAa,KAAK,CAAA,MAAK,EAAE,OAAO,OAAO,EAAE,GAAG;AAC7C,mBAAe,CAAC,GAAG,cAAc;AAAA,MAC7B,IAAI,OAAO;AAAA,MACX,MAAM,OAAO;AAAA,MACb,aAAa,OAAO;AAAA,MACpB,cAAc,OAAO;AAAA,IAAA,CACxB;AAAA,EACL;AACJ;AAGA,SAAS,qBAAqB,SAAgC;AAC1D,QAAM,SAAS,kBAAkB,IAAI,OAAO;AAC5C,MAAI,OAAQ,QAAO,EAAE,IAAI,OAAO,IAAI,MAAM,OAAO,MAAM,aAAa,OAAO,aAAa,cAAc,OAAO,aAAA;AAC7G,QAAM,QAAQ,QAAQ,MAAM,GAAG,EAAE,SAAS,SAAS,QAAQ,MAAM,GAAG;AACpE,SAAO,EAAE,IAAI,SAAS,MAAM,cAAc,CAAC,WAAW,EAAA;AAC1D;AAGA,IAAI,mBAAmB;AAMhB,SAAS,mBAAmB,KAAmB;AAClD,qBAAmB;AACvB;AAGO,SAAS,qBAA6B;AACzC,SAAO;AACX;AASA,IAAI,iBAAgC;AAE7B,SAAS,iBAAiB,GAAwB;AACrD,mBAAiB;AACrB;AAEO,SAAS,mBAAkC;AAC9C,SAAO;AACX;AAGA,eAAe,wBAAwB,aAAoC;AACvE,MAAI,qBAAqB,EAAG;AAC5B,MAAI;AACA,UAAM,SAAS,MAAM,iBAAA;AACrB,UAAM,SAAS,OAAO,OAAO,CAAA,MAAK,EAAE,YAAY,WAAW;AAC3D,UAAM,SAAS,OAAO,SAAS;AAC/B,QAAI,UAAU,EAAG;AAEjB,aAAS,IAAI,GAAG,IAAI,UAAU,IAAI,OAAO,QAAQ,KAAK;AAClD,YAAM,kBAAkB,OAAO,CAAC,EAAG,OAAO;AAAA,IAC9C;AAAA,EACJ,QAAQ;AAAA,EAA0B;AACtC;AAGA,eAAe,sBAAqC;AAChD,MAAI;AACA,UAAM,SAAS,MAAM,iBAAA;AACrB,eAAW,SAAS,QAAQ;AACxB,UAAI,CAAC,aAAa,KAAK,CAAA,MAAK,EAAE,OAAO,MAAM,OAAO,GAAG;AACjD,uBAAe,CAAC,GAAG,cAAc,qBAAqB,MAAM,OAAO,CAAC;AAAA,MACxE;AAAA,IACJ;AAAA,EACJ,QAAQ;AAAA,EAA0B;AACtC;AAGA,SAAS,WAAW,UAAgD;AAChE,QAAM,SAA0B,CAAA;AAChC,aAAW,KAAK,UAAU;AACtB,QAAI,EAAE,SAAS,UAAU,EAAE,SAAS,eAAe,EAAE,SAAS,UAAU;AACpE,YAAM,OAAO,cAAc,EAAE,OAAO;AACpC,UAAI,aAAa,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,MAAM;AAAA,IACzD;AAAA,EAEJ;AACA,SAAO;AACX;AAMA,IAAI,UAAyB;AAQ7B,MAAM,uCAAuB,IAAA;AAC7B,MAAM,wCAAwB,IAAA;AAM9B,IAAI,iBAAgC,QAAQ,QAAA;AAC5C,MAAM,6CAA6B,IAAA;AAGnC,SAAS,qBAAqB,IAAkB;AAC5C,QAAM,UAAU,uBAAuB,IAAI,EAAE;AAC7C,MAAI,SAAS;AACT,2BAAuB,OAAO,EAAE;AAChC,YAAA;AAAA,EACJ;AACJ;AAGA,IAAI,iBAAgC;AAEpC,IAAI,oBAAmC;AAEvC,SAAS,aAAqB;AAC1B,MAAI,CAAC,SAAS;AACV,cAAU,IAAI,OAAO,IAAA;AAAA;AAAA,MAAA;MAAA,YAAA;AAAA,IAAA,GAAyC,EAAE,MAAM,SAAA,CAAU;AAChF,YAAQ,iBAAiB,WAAW,oBAAoB;AACxD,YAAQ,iBAAiB,SAAS,kBAAkB;AACpD,YAAQ,iBAAiB,gBAAgB,kBAAkB;AAAA,EAC/D;AACA,SAAO;AACX;AAOA,SAAS,mBAAmB,GAAgB;AACxC,QAAM,UAAW,GAAkB,WAAW;AAE9C,aAAW,KAAK,iBAAiB,UAAU;AACvC,QAAI;AAAE,QAAE,OAAO,IAAI,MAAM,OAAO,CAAC;AAAA,IAAG,QAAQ;AAAA,IAAe;AAAA,EAC/D;AACA,mBAAiB,MAAA;AAEjB,aAAW,QAAQ,kBAAkB,UAAU;AAC3C,QAAI;AAAE,WAAK,QAAQ,EAAE,MAAM,SAAkB,SAAS;AAAG,WAAK,MAAA;AAAA,IAAS,QACjE;AAAA,IAAe;AAAA,EACzB;AACA,oBAAkB,MAAA;AAGlB,aAAW,WAAW,uBAAuB,UAAU;AACnD,QAAI;AAAE,cAAA;AAAA,IAAW,QAAQ;AAAA,IAAe;AAAA,EAC5C;AACA,yBAAuB,MAAA;AACvB,mBAAiB,QAAQ,QAAA;AAEzB,mBAAiB;AACjB,sBAAoB;AACpB,MAAI;AAAE,aAAS,UAAA;AAAA,EAAa,QAAQ;AAAA,EAAe;AACnD,YAAU;AACd;AAEA,SAAS,qBAAqB,OAA2B;AACrD,QAAM,MAAM,MAAM;AAElB,UAAQ,IAAI,MAAA;AAAA,IACR,KAAK,YAAY;AACb,YAAM,UAAU,iBAAiB,IAAI,IAAI,EAAE;AAC3C,UAAI,CAAC,QAAS;AACd,UAAI,IAAI,WAAW,SAAS;AACxB,gBAAQ,WAAW,EAAE,QAAQ,QAAA,CAAS;AACtC,gBAAQ,QAAA;AACR,yBAAiB,OAAO,IAAI,EAAE;AAAA,MAClC,WAAW,IAAI,WAAW,WAAW;AACjC,gBAAQ,WAAW,EAAE,QAAQ,UAAA,CAAW;AAAA,MAC5C,WAAW,IAAI,WAAW,UAAU;AAChC,gBAAQ,WAAW,EAAE,QAAQ,UAAU,MAAM,IAAI,MAAM,UAAU,IAAI,SAAA,CAAU;AAAA,MACnF,OAAO;AACH,gBAAQ,WAAW,EAAE,QAAQ,eAAe,MAAM,IAAI,MAAM,UAAU,IAAI,SAAA,CAAU;AAAA,MACxF;AACA;AAAA,IACJ;AAAA,IACA,KAAK,iBAAiB;AAClB,YAAM,UAAU,iBAAiB,IAAI,IAAI,EAAE;AAC3C,UAAI,CAAC,QAAS;AACd,cAAQ,OAAO,IAAI,MAAM,IAAI,OAAO,CAAC;AACrC,uBAAiB,OAAO,IAAI,EAAE;AAC9B,UAAI,sBAAsB,QAAQ,QAAS,qBAAoB;AAC/D;AAAA,IACJ;AAAA,IACA,KAAK,kBAAkB;AACnB,uBAAiB,IAAI;AACrB,0BAAoB;AAEpB,WAAK,wBAAwB,IAAI,OAAO,EAAE,KAAK,MAAM,qBAAqB;AAC1E;AAAA,IACJ;AAAA,IACA,KAAK,aAAa;AACd,YAAM,OAAO,kBAAkB,IAAI,IAAI,EAAE;AACzC,UAAI,CAAC,KAAM;AACX,WAAK,QAAQ,EAAE,MAAM,IAAI,WAAkC,OAAO,IAAI,OAAO;AAC7E;AAAA,IACJ;AAAA,IACA,KAAK,YAAY;AACb,2BAAqB,IAAI,EAAE;AAC3B,YAAM,OAAO,kBAAkB,IAAI,IAAI,EAAE;AACzC,UAAI,CAAC,KAAM;AACX,WAAK,QAAQ,EAAE,MAAM,QAAiB,GAAI,IAAI,QAAQ,EAAE,OAAO,IAAI,MAAA,IAAU,CAAA,GAAK;AAClF,WAAK,MAAA;AACL,wBAAkB,OAAO,IAAI,EAAE;AAC/B;AAAA,IACJ;AAAA,IACA,KAAK,aAAa;AACd,2BAAqB,IAAI,EAAE;AAC3B,YAAM,OAAO,kBAAkB,IAAI,IAAI,EAAE;AACzC,UAAI,CAAC,KAAM;AACX,WAAK,QAAQ,EAAE,MAAM,SAAkB,SAAS,IAAI,SAAS;AAC7D,WAAK,MAAA;AACL,wBAAkB,OAAO,IAAI,EAAE;AAC/B;AAAA,IACJ;AAAA,EAAA;AAER;AAEO,MAAM,uBAAyC;AAAA,EAClD,IAAI;AAAA,EAEJ,cAAc;AACV,WAAO;AAAA,MACH,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,MACP,aAAa;AAAA,MACb,eAAe;AAAA,MACf,SAAS;AAAA,MACT,SAAS;AAAA,IAAA;AAAA,EAEjB;AAAA,EAEA,YAA6B;AACzB,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,cAAwC;AAC1C,UAAM,oBAAA;AACN,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,KAAK,SAAyD;AAChE,UAAM,WAAW,WAAW,QAAQ,QAAQ;AAC5C,UAAM,YAAY,OAAO,WAAA;AACzB,UAAM,UAAU;AAAA,MACZ,WAAW,QAAQ;AAAA,MACnB,aAAa,QAAQ;AAAA,MACrB,MAAM,QAAQ;AAAA,IAAA;AAElB,UAAM,OAAO,kBAAkB,IAAI,QAAQ,OAAO,GAAG,QAAQ;AAK7D,UAAM,eAAe;AACrB,qBAAiB,IAAI,QAAc,CAAC,gBAAgB;AAChD,6BAAuB,IAAI,WAAW,WAAW;AAAA,IACrD,CAAC;AACD,UAAM,eAAe,MAAY;AAC7B,iBAAA,EAAa,YAAY;AAAA,QACrB,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,SAAS,QAAQ;AAAA,QACjB;AAAA,QACA;AAAA,QACA;AAAA,QACA,OAAO,kBAAkB,IAAI,QAAQ,OAAO,GAAG;AAAA,QAC/C,QAAQ;AAAA,MAAA,CACX;AAAA,IACL;AAEA,QAAI,QAAQ,WAAW,OAAO;AAC1B,aAAO,IAAI,QAAgB,CAAC,SAAS,WAAW;AAC5C,YAAI,SAAS;AACb,cAAM,WAAW;AAAA,UACb,SAAS,CAAC,UAA8D;AACpE,gBAAI,MAAM,SAAS,OAAQ,WAAU,MAAM,SAAS;AAAA,qBAC3C,MAAM,SAAS,OAAQ,SAAQ,MAAM;AAAA,qBACrC,MAAM,SAAS,QAAS,QAAO,IAAI,MAAM,MAAM,OAAO,CAAC;AAAA,UACpE;AAAA,UACA,OAAO,MAAM;AAAA,UAAc;AAAA,QAAA;AAE/B,0BAAkB,IAAI,WAAW,QAAQ;AACzC,aAAK,aAAa,KAAK,YAAY;AAAA,MACvC,CAAC;AAAA,IACL;AAEA,WAAO,IAAI,eAAe;AAAA,MACtB,MAAM,MAAM,YAAY;AACpB,0BAAkB,IAAI,WAAW,UAAU;AAC3C,cAAM;AACN,qBAAA;AAAA,MACJ;AAAA,MACA,SAAS;AACL,0BAAkB,OAAO,SAAS;AAKlC,mBAAA,EAAa,YAAY,EAAE,MAAM,UAAU,IAAI,WAAW;AAAA,MAC9D;AAAA,IAAA,CACH;AAAA,EACL;AAAA,EAEA,MAAM,eAAe,SAAuC;AACxD,QAAI,mBAAmB,QAAS,QAAO;AACvC,QAAI,sBAAsB,QAAS,QAAO;AAC1C,QAAI,YAAY,YAAY;AACxB,UAAI;AACA,cAAM,YAAY,mBAAmB,OAAO;AAC5C,cAAM,QAAQ,MAAM,OAAO,KAAA;AAC3B,mBAAW,QAAQ,OAAO;AACtB,gBAAM,QAAQ,MAAM,OAAO,KAAK,IAAI;AACpC,gBAAM,OAAO,MAAM,MAAM,KAAA;AACzB,cAAI,KAAK,KAAK,CAAA,MAAK,EAAE,IAAI,SAAS,SAAS,KAAK,EAAE,IAAI,SAAS,UAAU,GAAG,CAAC,GAAG;AAC5E,mBAAO;AAAA,UACX;AAAA,QACJ;AAAA,MACJ,QAAQ;AAAA,MAER;AAAA,IACJ;AACA,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,aAAa,SAAiB,YAA2D;AAC3F,QAAI,mBAAmB,SAAS;AAC5B,iBAAW,EAAE,QAAQ,SAAS;AAC9B;AAAA,IACJ;AAEA,UAAM,YAAY,OAAO,WAAA;AACzB,wBAAoB;AAEpB,UAAM,OAAO,kBAAkB,IAAI,OAAO,GAAG,QAAQ;AACrD,UAAM,QAAQ,kBAAkB,IAAI,OAAO,GAAG;AAC9C,WAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC1C,uBAAiB,IAAI,WAAW,EAAE,SAAS,YAAY,SAAS,QAAQ;AACxE,iBAAA,EAAa,YAAY,EAAE,MAAM,WAAW,IAAI,WAAW,SAAS,MAAM,OAAO,QAAQ,eAAA,CAAgB;AAAA,IAC7G,CAAC;AAAA,EACL;AAAA,EAEA,MAAM,YAAY,SAAgC;AAC9C,UAAM,kBAAkB,OAAO;AAAA,EACnC;AACJ;AAUO,SAAS,mBAAkC;AAC9C,SAAO;AACX;AAGO,SAAS,kBAAwB;AACpC,WAAS,UAAA;AACT,YAAU;AACV,mBAAiB;AACjB,sBAAoB;AACpB,aAAW,CAAA,EAAG,CAAC,KAAK,kBAAkB;AAClC,MAAE,OAAO,IAAI,MAAM,mBAAmB,CAAC;AAAA,EAC3C;AACA,mBAAiB,MAAA;AACjB,aAAW,CAAA,EAAG,IAAI,KAAK,mBAAmB;AACtC,QAAI;AAAE,WAAK,QAAQ,EAAE,MAAM,SAAkB,SAAS,qBAAqB;AAAG,WAAK,MAAA;AAAA,IAAS,QAAQ;AAAA,IAAuB;AAAA,EAC/H;AACA,oBAAkB,MAAA;AACtB;AAeA,eAAsB,mBAAgD;AAClE,MAAI,EAAE,YAAY,YAAa,QAAO,CAAA;AAEtC,QAAM,4BAAY,IAAA;AAIlB,WAAS,eAAe,KAA4B;AAChD,UAAM,IAAI,IAAI,MAAM,4CAA4C;AAChE,WAAO,IAAI,mBAAmB,EAAE,CAAC,CAAE,IAAI;AAAA,EAC3C;AAEA,WAAS,UAAU,SAAyB;AACxC,UAAM,SAAS,kBAAkB,IAAI,OAAO;AAC5C,QAAI,eAAe,OAAO;AAC1B,YAAQ,QAAQ,MAAM,GAAG,EAAE,SAAS,SAAS,QAAQ,MAAM,GAAG;AAAA,EAClE;AAEA,MAAI;AACA,UAAM,aAAa,MAAM,OAAO,KAAA;AAChC,UAAM,QAAQ,IAAI,WAAW,IAAI,OAAO,cAAc;AAClD,UAAI;AACA,cAAM,QAAQ,MAAM,OAAO,KAAK,SAAS;AACzC,cAAM,WAAW,MAAM,MAAM,KAAA;AAC7B,mBAAW,OAAO,UAAU;AACxB,gBAAM,UAAU,eAAe,IAAI,GAAG;AACtC,cAAI,CAAC,QAAS;AACd,cAAI,CAAC,MAAM,IAAI,OAAO,GAAG;AACrB,kBAAM,IAAI,SAAS,EAAE,MAAM,UAAU,OAAO,GAAG,WAAW,GAAG;AAAA,UACjE;AACA,gBAAM,WAAW,MAAM,MAAM,MAAM,GAAG;AACtC,cAAI,CAAC,SAAU;AACf,gBAAM,gBAAgB,SAAS,QAAQ,IAAI,gBAAgB;AAC3D,cAAI,eAAe;AACf,kBAAM,IAAI,OAAO,EAAG,aAAa,SAAS,eAAe,EAAE;AAAA,UAC/D,OAAO;AACH,gBAAI;AACA,oBAAM,OAAO,MAAM,SAAS,MAAA,EAAQ,KAAA;AACpC,oBAAM,IAAI,OAAO,EAAG,aAAa,KAAK;AAAA,YAC1C,QAAQ;AAAA,YAAa;AAAA,UACzB;AAAA,QACJ;AAAA,MACJ,QAAQ;AAAA,MAAgC;AAAA,IAC5C,CAAC,CAAC;AAAA,EACN,QAAQ;AACJ,WAAO,CAAA;AAAA,EACX;AAEA,SAAO,MAAM,KAAK,MAAM,QAAA,CAAS,EAAE,IAAI,CAAC,CAAC,SAAS,EAAE,MAAM,UAAA,CAAW,OAAO;AAAA,IACxE;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,mBAAmB;AAAA,EAAA,EAC7B;AACN;AAMA,eAAsB,kBAAkB,SAAgC;AACpE,MAAI,mBAAmB,WAAW,sBAAsB,SAAS;AAC7D,oBAAA;AAAA,EACJ;AACA,MAAI,EAAE,YAAY,YAAa;AAC/B,MAAI;AACA,UAAM,aAAa,MAAM,OAAO,KAAA;AAChC,UAAM,QAAQ,IAAI,WAAW,IAAI,OAAO,cAAc;AAClD,UAAI;AACA,cAAM,QAAQ,MAAM,OAAO,KAAK,SAAS;AACzC,cAAM,WAAW,MAAM,MAAM,KAAA;AAC7B,cAAM,UAAU,mBAAmB,OAAO;AAC1C,cAAM,QAAQ;AAAA,UACV,SACK,OAAO,CAAA,MAAK,EAAE,IAAI,SAAS,OAAO,KAAK,EAAE,IAAI,SAAS,OAAO,CAAC,EAC9D,IAAI,OAAK,MAAM,OAAO,CAAC,CAAC;AAAA,QAAA;AAAA,MAErC,QAAQ;AAAA,MAAa;AAAA,IACzB,CAAC,CAAC;AAAA,EACN,QAAQ;AAAA,EAA8B;AAC1C;"}
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Transformers.js inference worker.
|
|
3
|
+
*
|
|
4
|
+
* Runs entirely off the main thread. It holds ONE text-generation pipeline at a
|
|
5
|
+
* time and speaks a tiny postMessage protocol with the provider on the main
|
|
6
|
+
* thread (see `index.ts`):
|
|
7
|
+
*
|
|
8
|
+
* main → worker : { type: 'prepare', id, modelId, dtype?, device? }
|
|
9
|
+
* { type: 'generate', id, modelId, messages, options, dtype?, device? }
|
|
10
|
+
* worker → main : { type: 'progress', id, status, file?, progress? }
|
|
11
|
+
* { type: 'prepare-error', id, message }
|
|
12
|
+
* { type: 'pipeline-ready', modelId }
|
|
13
|
+
* { type: 'gen-chunk', id, chunkType: 'text', delta }
|
|
14
|
+
* { type: 'gen-done', id }
|
|
15
|
+
* { type: 'gen-error', id, message }
|
|
16
|
+
*
|
|
17
|
+
* Deliberately generic: no vision, no low-level ORT session management, no
|
|
18
|
+
* model-family specifics — just the high-level `pipeline()` + `TextStreamer`.
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aparte/provider-transformers",
|
|
3
|
+
"version": "0.2.0-alpha.0",
|
|
4
|
+
"description": "Run LLMs 100% in the browser via Transformers.js (WebGPU/WASM) — a local, keyless AI provider for aparté. Streams tokens off the main thread in a Web Worker.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"@aparte-workspace/source": "./src/index.ts",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@huggingface/transformers": "^4.2.0",
|
|
28
|
+
"@aparte/core": "0.2.0-alpha.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@huggingface/transformers": "^4.2.0",
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"typescript": "^5.4.0",
|
|
34
|
+
"vite": "^6.0.0",
|
|
35
|
+
"vite-plugin-dts": "^4.5.4",
|
|
36
|
+
"@aparte/core": "0.2.0-alpha.0"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"ai",
|
|
40
|
+
"provider",
|
|
41
|
+
"transformers.js",
|
|
42
|
+
"webgpu",
|
|
43
|
+
"wasm",
|
|
44
|
+
"local",
|
|
45
|
+
"in-browser",
|
|
46
|
+
"llm"
|
|
47
|
+
],
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/apartejs/aparte.git",
|
|
52
|
+
"directory": "packages/providers/ai/transformers"
|
|
53
|
+
},
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/apartejs/aparte/issues"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"dev": "vite",
|
|
59
|
+
"build": "vite build && tsc -b --emitDeclarationOnly --force",
|
|
60
|
+
"preview": "vite preview",
|
|
61
|
+
"test": "vitest",
|
|
62
|
+
"test:run": "vitest run",
|
|
63
|
+
"test:coverage": "vitest run --coverage"
|
|
64
|
+
}
|
|
65
|
+
}
|