@tanstack-ai-browser/wllama 0.2.1 → 0.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack-ai-browser/wllama",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "TanStack AI adapter for GGUF models running in the browser with wllama (llama.cpp in WebAssembly)",
5
5
  "license": "MIT",
6
6
  "author": "daveycodez",
@@ -55,11 +55,11 @@
55
55
  "@tanstack/ai": "1.4.0",
56
56
  "esbuild": "0.28.2"
57
57
  },
58
- "scripts": {
59
- "test": "node --import @swc-node/register/esm-register --test \"src/**/*.test.ts\""
60
- },
61
58
  "dependencies": {
62
59
  "@tanstack/openai-base": "^1.0.4",
63
60
  "@wllama/wllama": "3.13.0"
61
+ },
62
+ "scripts": {
63
+ "test": "node --import @swc-node/register/esm-register --test \"src/**/*.test.ts\""
64
64
  }
65
- }
65
+ }
@@ -1,46 +0,0 @@
1
- /**
2
- * The memory the page's models take, shared by this project's runtimes
3
- * (Transformers.js, WebGL, wllama), each of which loads models in its own
4
- * worker. Before a runtime loads a model it claims the memory: other
5
- * runtimes' least recently used models are unloaded until everything fits
6
- * the budget. A phone can't hold two 1 GB models (an iPhone Safari tab has
7
- * 2 GB), and small models still stay loaded side by side.
8
- *
9
- * The packages each carry this file and share one registry through
10
- * globalThis, so they find each other even when bundled apart. Keep the
11
- * copies identical (packages/{transformers-js,webgl,wllama}/src).
12
- */
13
- /**
14
- * The default budget: 1.5 GB on iOS (a Safari tab gets 2 GB, the page and
15
- * Safari take the rest), half the device's memory where the browser says
16
- * (Chrome: up to 8 GB), else 4 GB.
17
- */
18
- export declare function defaultMemoryBudget(): number;
19
- /**
20
- * Sets how many bytes the page's models may take together, across this
21
- * project's runtimes, from the next load on; undefined for the default.
22
- */
23
- export declare function setMemoryBudget(bytes: number | undefined): void;
24
- /**
25
- * Makes room for `owner`'s model of about `bytes`, unloading other runtimes'
26
- * models, least recently used first, until it fits the budget (models kept
27
- * loaded stay), then records it in place of `owner`'s previous one.
28
- * Resolves to false, recording nothing, if `signal` aborted meanwhile.
29
- */
30
- export declare function claimMemory(model: {
31
- owner: string;
32
- model: string;
33
- bytes: number;
34
- keepLoaded: boolean;
35
- unload: () => unknown;
36
- /**
37
- * The load the claim is for: once it's aborted (canceled or replaced),
38
- * nothing more is unloaded for it and nothing is recorded.
39
- */
40
- signal?: AbortSignal;
41
- }): Promise<boolean>;
42
- /** Marks `owner`'s model as just used (for least recently used order). */
43
- export declare function touchMemory(owner: string): void;
44
- /** Forgets `owner`'s model, once it's unloaded or failed to load. */
45
- export declare function releaseMemory(owner: string): void;
46
- //# sourceMappingURL=model-memory.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"model-memory.d.ts","sourceRoot":"","sources":["../src/model-memory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA6BH;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAS5C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,QAExD;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE;IACvC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,OAAO,CAAA;IACnB,MAAM,EAAE,MAAM,OAAO,CAAA;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAkBnB;AAED,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,QAGxC;AAED,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,QAE1C"}
@@ -1,79 +0,0 @@
1
- /**
2
- * The memory the page's models take, shared by this project's runtimes
3
- * (Transformers.js, WebGL, wllama), each of which loads models in its own
4
- * worker. Before a runtime loads a model it claims the memory: other
5
- * runtimes' least recently used models are unloaded until everything fits
6
- * the budget. A phone can't hold two 1 GB models (an iPhone Safari tab has
7
- * 2 GB), and small models still stay loaded side by side.
8
- *
9
- * The packages each carry this file and share one registry through
10
- * globalThis, so they find each other even when bundled apart. Keep the
11
- * copies identical (packages/{transformers-js,webgl,wllama}/src).
12
- */
13
- const KEY = Symbol.for("@tanstack-ai-browser/model-memory");
14
- function registry() {
15
- const scope = globalThis;
16
- return (scope[KEY] ??= { version: 1, held: new Map() });
17
- }
18
- /**
19
- * The default budget: 1.5 GB on iOS (a Safari tab gets 2 GB, the page and
20
- * Safari take the rest), half the device's memory where the browser says
21
- * (Chrome: up to 8 GB), else 4 GB.
22
- */
23
- export function defaultMemoryBudget() {
24
- if (typeof navigator === "undefined")
25
- return Number.POSITIVE_INFINITY;
26
- const { userAgent, maxTouchPoints } = navigator;
27
- const ios = /iPhone|iPad|iPod/.test(userAgent) ||
28
- (/Macintosh/.test(userAgent) && maxTouchPoints > 1);
29
- if (ios)
30
- return 1.5e9;
31
- const gigabytes = navigator.deviceMemory;
32
- return gigabytes ? gigabytes * 0.5e9 : 4e9;
33
- }
34
- /**
35
- * Sets how many bytes the page's models may take together, across this
36
- * project's runtimes, from the next load on; undefined for the default.
37
- */
38
- export function setMemoryBudget(bytes) {
39
- registry().budget = bytes;
40
- }
41
- /**
42
- * Makes room for `owner`'s model of about `bytes`, unloading other runtimes'
43
- * models, least recently used first, until it fits the budget (models kept
44
- * loaded stay), then records it in place of `owner`'s previous one.
45
- * Resolves to false, recording nothing, if `signal` aborted meanwhile.
46
- */
47
- export async function claimMemory(model) {
48
- const { signal, ...entry } = model;
49
- if (signal?.aborted)
50
- return false;
51
- const { held, budget } = registry();
52
- const limit = budget ?? defaultMemoryBudget();
53
- held.delete(entry.owner);
54
- const others = [...held.values()].sort((a, b) => a.lastUsed - b.lastUsed);
55
- let total = others.reduce((sum, h) => sum + h.bytes, entry.bytes);
56
- for (const other of others) {
57
- if (total <= limit)
58
- break;
59
- if (other.keepLoaded)
60
- continue;
61
- held.delete(other.owner);
62
- total -= other.bytes;
63
- await other.unload();
64
- if (signal?.aborted)
65
- return false;
66
- }
67
- held.set(entry.owner, { ...entry, lastUsed: Date.now() });
68
- return true;
69
- }
70
- /** Marks `owner`'s model as just used (for least recently used order). */
71
- export function touchMemory(owner) {
72
- const held = registry().held.get(owner);
73
- if (held)
74
- held.lastUsed = Date.now();
75
- }
76
- /** Forgets `owner`'s model, once it's unloaded or failed to load. */
77
- export function releaseMemory(owner) {
78
- registry().held.delete(owner);
79
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * The GGUF models this package lists, with what each supports and its
3
- * reasoning levels.
4
- */
5
- export { WLLAMA_CHAT_MODELS, WLLAMA_MODEL_META, WLLAMA_MODEL_REASONING } from "./catalog.js";
6
- export type { WllamaChatModel, WllamaModelMeta, WllamaModelReasoningByName } from "./catalog.js";
7
- //# sourceMappingURL=model-meta.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"model-meta.d.ts","sourceRoot":"","sources":["../src/model-meta.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,sBAAsB,EACvB,MAAM,cAAc,CAAA;AACrB,YAAY,EACV,eAAe,EACf,eAAe,EACf,0BAA0B,EAC3B,MAAM,cAAc,CAAA"}
@@ -1,5 +0,0 @@
1
- /**
2
- * The GGUF models this package lists, with what each supports and its
3
- * reasoning levels.
4
- */
5
- export { WLLAMA_CHAT_MODELS, WLLAMA_MODEL_META, WLLAMA_MODEL_REASONING } from "./catalog.js";
@@ -1,48 +0,0 @@
1
- /**
2
- * The callers waiting on one shared model load: their progress callbacks
3
- * and how many still wait. A caller whose signal aborts stops waiting at
4
- * once and hears nothing more; when the last caller still waiting aborts,
5
- * the load is cancelled. (Kept in step with the same file in the other
6
- * runtime packages.)
7
- */
8
- /** A download's progress, from 0 to 1. */
9
- type DownloadProgressCallback = (progress: number) => void;
10
- /** What an aborted `create()` rejects with. */
11
- export declare function abortError(): DOMException;
12
- /** Calls a progress callback; one that throws doesn't stop the load. */
13
- export declare function callProgress(listener: DownloadProgressCallback, progress: number): void;
14
- export type WaitOptions = {
15
- onDownloadProgress?: DownloadProgressCallback;
16
- signal?: AbortSignal;
17
- /**
18
- * Whether this caller aborting last cancels the load (true by default).
19
- * A chat's own wait passes false: it stops waiting, and the download goes
20
- * on for the next chat.
21
- */
22
- cancels?: boolean;
23
- /** What this caller rejects with when it aborts: an AbortError by default. */
24
- reason?: (signal: AbortSignal) => unknown;
25
- };
26
- export declare class LoadWaiters {
27
- private readonly cancel;
28
- /** Each callback, with how many waiting callers use it: called once per report. */
29
- private readonly callbacks;
30
- private waiting;
31
- private settled;
32
- /** `cancel` stops the load: called when the last caller still waiting aborts. */
33
- constructor(cancel: () => void);
34
- /** Reports progress to the callers still waiting. */
35
- report(progress: number): void;
36
- /** The load settled (ready, failed or replaced): nothing more to report or cancel. */
37
- settle(): void;
38
- /**
39
- * `load`'s result for one more caller. Aborting `signal` rejects this
40
- * caller at once and stops its reports; if it was the last caller waiting
41
- * (and `cancels`), the load is cancelled. The promise is fine to leave
42
- * unawaited. Check the signal before starting the load: an aborted one
43
- * rejects here without waiting.
44
- */
45
- wait<T>(load: Promise<T>, options?: WaitOptions): Promise<T>;
46
- }
47
- export {};
48
- //# sourceMappingURL=shared-load.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"shared-load.d.ts","sourceRoot":"","sources":["../src/shared-load.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,0CAA0C;AAC1C,KAAK,wBAAwB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;AAE1D,+CAA+C;AAC/C,wBAAgB,UAAU,IAAI,YAAY,CAEzC;AAED,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,QAAQ,EAAE,wBAAwB,EAAE,QAAQ,EAAE,MAAM,QAMhF;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,kBAAkB,CAAC,EAAE,wBAAwB,CAAA;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAA;CAC1C,CAAA;AAED,qBAAa,WAAW;IAOV,OAAO,CAAC,QAAQ,CAAC,MAAM;IANnC,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8C;IACxE,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,OAAO,CAAQ;IAEvB,iFAAiF;IACjF,YAA6B,MAAM,EAAE,MAAM,IAAI,EAAI;IAEnD,qDAAqD;IACrD,MAAM,CAAC,QAAQ,EAAE,MAAM,QAEtB;IAED,sFAAsF;IACtF,MAAM,SAIL;IAED;;;;;;OAMG;IACH,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,CAAC,CAAC,CA6C/D;CACF"}
@@ -1,103 +0,0 @@
1
- /**
2
- * The callers waiting on one shared model load: their progress callbacks
3
- * and how many still wait. A caller whose signal aborts stops waiting at
4
- * once and hears nothing more; when the last caller still waiting aborts,
5
- * the load is cancelled. (Kept in step with the same file in the other
6
- * runtime packages.)
7
- */
8
- /** What an aborted `create()` rejects with. */
9
- export function abortError() {
10
- return new DOMException("The model load was aborted.", "AbortError");
11
- }
12
- /** Calls a progress callback; one that throws doesn't stop the load. */
13
- export function callProgress(listener, progress) {
14
- try {
15
- listener(progress);
16
- }
17
- catch (error) {
18
- console.error(error);
19
- }
20
- }
21
- export class LoadWaiters {
22
- cancel;
23
- /** Each callback, with how many waiting callers use it: called once per report. */
24
- callbacks = new Map();
25
- waiting = 0;
26
- settled = false;
27
- /** `cancel` stops the load: called when the last caller still waiting aborts. */
28
- constructor(cancel) {
29
- this.cancel = cancel;
30
- }
31
- /** Reports progress to the callers still waiting. */
32
- report(progress) {
33
- for (const listener of [...this.callbacks.keys()])
34
- callProgress(listener, progress);
35
- }
36
- /** The load settled (ready, failed or replaced): nothing more to report or cancel. */
37
- settle() {
38
- this.settled = true;
39
- this.callbacks.clear();
40
- this.waiting = 0;
41
- }
42
- /**
43
- * `load`'s result for one more caller. Aborting `signal` rejects this
44
- * caller at once and stops its reports; if it was the last caller waiting
45
- * (and `cancels`), the load is cancelled. The promise is fine to leave
46
- * unawaited. Check the signal before starting the load: an aborted one
47
- * rejects here without waiting.
48
- */
49
- wait(load, options = {}) {
50
- const { onDownloadProgress, signal, cancels = true, reason = abortError } = options;
51
- if (signal?.aborted)
52
- return quiet(Promise.reject(reason(signal)));
53
- if (this.settled)
54
- return quiet(load);
55
- this.waiting++;
56
- if (onDownloadProgress) {
57
- this.callbacks.set(onDownloadProgress, (this.callbacks.get(onDownloadProgress) ?? 0) + 1);
58
- }
59
- let left = false;
60
- const leave = () => {
61
- if (left)
62
- return;
63
- left = true;
64
- if (this.settled)
65
- return;
66
- this.waiting--;
67
- if (!onDownloadProgress)
68
- return;
69
- const count = (this.callbacks.get(onDownloadProgress) ?? 0) - 1;
70
- if (count > 0)
71
- this.callbacks.set(onDownloadProgress, count);
72
- else
73
- this.callbacks.delete(onDownloadProgress);
74
- };
75
- if (!signal) {
76
- load.then(leave, leave);
77
- return quiet(load);
78
- }
79
- return quiet(new Promise((resolve, reject) => {
80
- const onAbort = () => {
81
- leave();
82
- reject(reason(signal));
83
- if (cancels && !this.settled && this.waiting === 0)
84
- this.cancel();
85
- };
86
- signal.addEventListener("abort", onAbort, { once: true });
87
- load.then((value) => {
88
- signal.removeEventListener("abort", onAbort);
89
- leave();
90
- resolve(value);
91
- }, (error) => {
92
- signal.removeEventListener("abort", onAbort);
93
- leave();
94
- reject(error);
95
- });
96
- }));
97
- }
98
- }
99
- /** `promise`, with a rejection that's fine to leave unhandled. */
100
- function quiet(promise) {
101
- promise.catch(() => undefined);
102
- return promise;
103
- }
package/dist/store.d.ts DELETED
@@ -1,8 +0,0 @@
1
- /** A value that can be read, set and watched. Framework-free. */
2
- export type Store<T> = {
3
- get: () => T;
4
- set: (value: T) => void;
5
- subscribe: (listener: () => void) => () => void;
6
- };
7
- export declare function createStore<T>(initial: T): Store<T>;
8
- //# sourceMappingURL=store.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI;IACrB,GAAG,EAAE,MAAM,CAAC,CAAA;IACZ,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAA;IACvB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAA;CAChD,CAAA;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAgBnD"}
package/dist/store.js DELETED
@@ -1,17 +0,0 @@
1
- export function createStore(initial) {
2
- let value = initial;
3
- const listeners = new Set();
4
- return {
5
- get: () => value,
6
- set: (next) => {
7
- value = next;
8
- listeners.forEach((listener) => listener());
9
- },
10
- subscribe: (listener) => {
11
- listeners.add(listener);
12
- return () => {
13
- listeners.delete(listener);
14
- };
15
- }
16
- };
17
- }
@@ -1,26 +0,0 @@
1
- /** A chat turn as LFM2.5's chat template takes it. */
2
- export type ChatTurn = {
3
- role: "system" | "user" | "assistant" | "tool";
4
- /**
5
- * The text. An assistant turn's is the reply as the model wrote it:
6
- * thinking (`<think>…</think>`) and tool-call markup included. The
7
- * template drops the thinking of all but the last assistant turn.
8
- */
9
- content: string;
10
- };
11
- /** A tool as chat templates take it (OpenAI's function format). */
12
- export type ToolSpec = {
13
- type: "function";
14
- function: {
15
- name: string;
16
- description: string;
17
- parameters: unknown;
18
- };
19
- };
20
- /**
21
- * The prompt for `chat`, ending where the model's reply starts (or where
22
- * the chat ends, without `reply`). The template's BOS token is left out:
23
- * llama.cpp adds it when it reads the prompt.
24
- */
25
- export declare function renderChat(source: string, chat: Array<ChatTurn>, tools: Array<ToolSpec>, reply?: boolean): string;
26
- //# sourceMappingURL=template.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../src/template.ts"],"names":[],"mappings":"AAMA,sDAAsD;AACtD,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAA;IAC9C;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,mEAAmE;AACnE,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,UAAU,CAAA;IAChB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAA;CACrE,CAAA;AAID;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,EACrB,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,EACtB,KAAK,UAAO,UAcb"}
package/dist/template.js DELETED
@@ -1,25 +0,0 @@
1
- /**
2
- * The prompt for a chat, written by the model's own chat template (the
3
- * Jinja template in its GGUF file), the way Transformers renders it.
4
- */
5
- import { Template } from "@huggingface/jinja";
6
- const templates = new Map();
7
- /**
8
- * The prompt for `chat`, ending where the model's reply starts (or where
9
- * the chat ends, without `reply`). The template's BOS token is left out:
10
- * llama.cpp adds it when it reads the prompt.
11
- */
12
- export function renderChat(source, chat, tools, reply = true) {
13
- let template = templates.get(source);
14
- if (!template) {
15
- template = new Template(source);
16
- templates.set(source, template);
17
- }
18
- return template.render({
19
- messages: chat,
20
- ...(tools.length > 0 && { tools }),
21
- add_generation_prompt: reply,
22
- bos_token: "",
23
- eos_token: ""
24
- });
25
- }
@@ -1,39 +0,0 @@
1
- /**
2
- * Splitting a reply into thinking and answer. Copied from
3
- * @tanstack-ai-browser/transformers-js (thinking.ts), so this package
4
- * doesn't depend on that one and its worker.
5
- */
6
- /** Where a model's thinking starts and ends in its reply. */
7
- export type ThinkingMarkers = {
8
- open: string;
9
- close: string;
10
- };
11
- /** `<think>…</think>`, as LFM models (and most others) write it. */
12
- export declare const THINK_TAGS: ThinkingMarkers;
13
- /**
14
- * Splits a model's reply into its thinking and its answer. Thinking is any
15
- * `<think>…</think>` block (or the format's own markers), or everything
16
- * before a lone close (models prompted with an empty `<think></think>`
17
- * sometimes still think a little and close it). An open with no close yet
18
- * means it's still thinking.
19
- */
20
- export declare function splitThinking(text: string, { open: OPEN, close: CLOSE }?: ThinkingMarkers): {
21
- thinking: string;
22
- answer: string;
23
- isThinking: boolean;
24
- };
25
- export type ReplyPiece = {
26
- kind: "thinking" | "answer";
27
- text: string;
28
- };
29
- /**
30
- * Splits a reply into thinking and answer while it streams. A reply that
31
- * starts with the open marker (`<think>`) is thinking until the close
32
- * (`</think>`); the rest is the answer. Markers split across tokens are held
33
- * back until they are complete.
34
- */
35
- export declare function createThinkingSplitter({ open: OPEN, close: CLOSE }?: ThinkingMarkers): {
36
- push: (text: string) => Array<ReplyPiece>;
37
- flush: () => Array<ReplyPiece>;
38
- };
39
- //# sourceMappingURL=thinking.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"thinking.d.ts","sourceRoot":"","sources":["../src/thinking.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,6DAA6D;AAC7D,MAAM,MAAM,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAE7D,oEAAoE;AACpE,eAAO,MAAM,UAAU,EAAE,eAGxB,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,GAAE,eAA4B;;;;EAmC3D;AAUD,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,UAAU,GAAG,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtE;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK,EACb,GAAE,eAA4B;iBAeT,MAAM,KAAG,KAAK,CAAC,UAAU,CAAC;iBA6C5B,KAAK,CAAC,UAAU,CAAC;EAUpC"}
package/dist/thinking.js DELETED
@@ -1,140 +0,0 @@
1
- /**
2
- * Splitting a reply into thinking and answer. Copied from
3
- * @tanstack-ai-browser/transformers-js (thinking.ts), so this package
4
- * doesn't depend on that one and its worker.
5
- */
6
- /** `<think>…</think>`, as LFM models (and most others) write it. */
7
- export const THINK_TAGS = {
8
- open: "<think>",
9
- close: "</think>"
10
- };
11
- /**
12
- * Splits a model's reply into its thinking and its answer. Thinking is any
13
- * `<think>…</think>` block (or the format's own markers), or everything
14
- * before a lone close (models prompted with an empty `<think></think>`
15
- * sometimes still think a little and close it). An open with no close yet
16
- * means it's still thinking.
17
- */
18
- export function splitThinking(text, { open: OPEN, close: CLOSE } = THINK_TAGS) {
19
- const thoughts = [];
20
- let rest = text;
21
- let isThinking = false;
22
- for (;;) {
23
- const open = rest.indexOf(OPEN);
24
- const close = rest.indexOf(CLOSE);
25
- if (close !== -1 && (open === -1 || close < open)) {
26
- // A close with no open: everything before it was thinking.
27
- thoughts.push(rest.slice(0, close));
28
- rest = rest.slice(close + CLOSE.length);
29
- continue;
30
- }
31
- if (open === -1)
32
- break;
33
- const before = rest.slice(0, open);
34
- const after = rest.slice(open + OPEN.length);
35
- const end = after.indexOf(CLOSE);
36
- if (end === -1) {
37
- thoughts.push(after);
38
- rest = before;
39
- isThinking = true;
40
- break;
41
- }
42
- thoughts.push(after.slice(0, end));
43
- rest = before + after.slice(end + CLOSE.length);
44
- }
45
- return {
46
- thinking: thoughts
47
- .map((t) => t.trim())
48
- .filter(Boolean)
49
- .join("\n\n"),
50
- answer: rest.trim(),
51
- isThinking
52
- };
53
- }
54
- /** Length of the longest end of `text` that could be the start of `tag`. */
55
- function partialTag(text, tag) {
56
- for (let n = Math.min(tag.length - 1, text.length); n > 0; n--) {
57
- if (tag.startsWith(text.slice(-n)))
58
- return n;
59
- }
60
- return 0;
61
- }
62
- /**
63
- * Splits a reply into thinking and answer while it streams. A reply that
64
- * starts with the open marker (`<think>`) is thinking until the close
65
- * (`</think>`); the rest is the answer. Markers split across tokens are held
66
- * back until they are complete.
67
- */
68
- export function createThinkingSplitter({ open: OPEN, close: CLOSE } = THINK_TAGS) {
69
- let state = "start";
70
- let buffer = "";
71
- /** Whether the thinking has had any text yet (to drop a leading newline). */
72
- let thought = false;
73
- /** Whether the answer has had any text yet (to drop leading blank lines). */
74
- let answered = false;
75
- const answer = (text) => {
76
- const out = answered ? text : text.trimStart();
77
- if (!out)
78
- return [];
79
- answered = true;
80
- return [{ kind: "answer", text: out }];
81
- };
82
- function push(text) {
83
- buffer += text;
84
- const pieces = [];
85
- for (;;) {
86
- if (state === "start") {
87
- const lead = buffer.trimStart();
88
- if (lead.startsWith(OPEN)) {
89
- buffer = lead.slice(OPEN.length);
90
- state = "thinking";
91
- }
92
- else if (OPEN.startsWith(lead)) {
93
- return pieces;
94
- }
95
- else {
96
- state = "answer";
97
- }
98
- }
99
- else if (state === "thinking") {
100
- const end = buffer.indexOf(CLOSE);
101
- if (end === -1) {
102
- const keep = partialTag(buffer, CLOSE);
103
- const done = buffer.slice(0, buffer.length - keep);
104
- buffer = buffer.slice(done.length);
105
- const text = thought ? done : done.trimStart();
106
- if (text) {
107
- thought = true;
108
- pieces.push({ kind: "thinking", text });
109
- }
110
- return pieces;
111
- }
112
- const text = thought
113
- ? buffer.slice(0, end)
114
- : buffer.slice(0, end).trimStart();
115
- if (text) {
116
- thought = true;
117
- pieces.push({ kind: "thinking", text });
118
- }
119
- buffer = buffer.slice(end + CLOSE.length);
120
- state = "answer";
121
- }
122
- else {
123
- pieces.push(...answer(buffer));
124
- buffer = "";
125
- return pieces;
126
- }
127
- }
128
- }
129
- /** Whatever is still held back, once the reply has ended. */
130
- function flush() {
131
- const rest = buffer;
132
- buffer = "";
133
- if (!rest)
134
- return [];
135
- return state === "thinking"
136
- ? [{ kind: "thinking", text: rest }]
137
- : answer(rest);
138
- }
139
- return { push, flush };
140
- }
@@ -1,33 +0,0 @@
1
- /**
2
- * Reading and writing tool calls the way LFM2 and LFM2.5 write them:
3
- * Pythonic calls between markers,
4
- * `<|tool_call_start|>[get_weather(city="Tokyo")]<|tool_call_end|>`.
5
- *
6
- * Copied from @tanstack-ai-browser/transformers-js (tool-calls.ts, its LFM2
7
- * parts), so this package doesn't depend on that one and its worker.
8
- */
9
- export type ParsedToolCall = {
10
- name: string;
11
- args: Record<string, unknown>;
12
- };
13
- /** How a model writes tool calls. */
14
- export type ToolFormat = "lfm2";
15
- export declare const TOOL_CALL_START = "<|tool_call_start|>";
16
- export declare const TOOL_CALL_END = "<|tool_call_end|>";
17
- /** Every tool call in `text`. */
18
- export declare function parseToolCalls(text: string): Array<ParsedToolCall>;
19
- /**
20
- * Splits a streaming answer into the text before the first tool call (passed
21
- * through as it arrives) and the tool-call markup after it (held back and
22
- * parsed once the reply ends).
23
- */
24
- export declare function createToolCallSplitter(markers?: Array<string>): {
25
- push: (text: string) => string;
26
- flush: () => {
27
- text: string;
28
- markup: string | null;
29
- };
30
- };
31
- /** A past tool call in LFM2.5's own format, for the assistant turn's text. */
32
- export declare function formatLfm2ToolCalls(calls: Array<ParsedToolCall>): string;
33
- //# sourceMappingURL=tool-calls.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"tool-calls.d.ts","sourceRoot":"","sources":["../src/tool-calls.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAA;AAE5E,qCAAqC;AACrC,MAAM,MAAM,UAAU,GAAG,MAAM,CAAA;AAE/B,eAAO,MAAM,eAAe,wBAAwB,CAAA;AACpD,eAAO,MAAM,aAAa,sBAAsB,CAAA;AA+GhD,iCAAiC;AACjC,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,cAAc,CAAC,CAQlE;AAWD;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,GAAE,KAAK,CAAC,MAAM,CAAqB;iBAMtB,MAAM;;;;;EA6B3B;AAUD,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,CAAC,cAAc,CAAC,UAU/D"}
@@ -1,203 +0,0 @@
1
- /**
2
- * Reading and writing tool calls the way LFM2 and LFM2.5 write them:
3
- * Pythonic calls between markers,
4
- * `<|tool_call_start|>[get_weather(city="Tokyo")]<|tool_call_end|>`.
5
- *
6
- * Copied from @tanstack-ai-browser/transformers-js (tool-calls.ts, its LFM2
7
- * parts), so this package doesn't depend on that one and its worker.
8
- */
9
- export const TOOL_CALL_START = "<|tool_call_start|>";
10
- export const TOOL_CALL_END = "<|tool_call_end|>";
11
- /** A Python-style or JSON value: "x", 'x', 1.5, True, None, [1, 2]… */
12
- function parseValue(raw) {
13
- const s = raw.trim();
14
- if (/^(["']).*\1$/s.test(s)) {
15
- try {
16
- return JSON.parse(s[0] === "'" ? `"${s.slice(1, -1)}"` : s);
17
- }
18
- catch {
19
- return s.slice(1, -1);
20
- }
21
- }
22
- if (s === "True" || s === "true")
23
- return true;
24
- if (s === "False" || s === "false")
25
- return false;
26
- if (s === "None" || s === "null")
27
- return null;
28
- if (s !== "" && !Number.isNaN(Number(s)))
29
- return Number(s);
30
- try {
31
- return JSON.parse(s);
32
- }
33
- catch {
34
- return s;
35
- }
36
- }
37
- /** After a value: the next argument (`, name=`), or the end of the call. */
38
- const VALUE_END = /^\s*(?:,\s*[A-Za-z_]\w*\s*=|,?\s*\)|$)/;
39
- /**
40
- * Where the string starting at `body[start]` ends (the index of its closing
41
- * quote). The model doesn't escape quotes inside its strings ('JWST's
42
- * findings'), so a quote only closes the string when the argument ends
43
- * after it; otherwise it's part of the text.
44
- */
45
- function stringEnd(body, start) {
46
- const quote = body[start];
47
- let last = -1;
48
- for (let i = start + 1; i < body.length; i++) {
49
- if (body[i] === "\\")
50
- i++;
51
- else if (body[i] === quote) {
52
- if (VALUE_END.test(body.slice(i + 1)))
53
- return i;
54
- last = i;
55
- }
56
- }
57
- return last;
58
- }
59
- /** A Python string literal's text, `\'` and `\"` included. */
60
- function stringText(raw) {
61
- const inner = raw.slice(1, -1);
62
- try {
63
- const escaped = inner.replace(/\\'/g, "'").replace(/(^|[^\\])"/g, '$1\\"');
64
- return JSON.parse(`"${escaped}"`);
65
- }
66
- catch {
67
- return inner;
68
- }
69
- }
70
- /** Where a `[…]` or `{…}` starting at `body[start]` ends, strings skipped. */
71
- function bracketEnd(body, start) {
72
- let depth = 0;
73
- for (let i = start; i < body.length; i++) {
74
- const c = body[i];
75
- if (c === '"' || c === "'") {
76
- const end = stringEnd(body, i);
77
- if (end < 0)
78
- return -1;
79
- i = end;
80
- }
81
- else if (c === "[" || c === "{")
82
- depth++;
83
- else if ((c === "]" || c === "}") && --depth === 0)
84
- return i;
85
- }
86
- return -1;
87
- }
88
- /** `[name(a="x", b=2), other()]` → calls. */
89
- function parsePythonic(body) {
90
- const calls = [];
91
- const callStart = /([A-Za-z_][\w.-]*)\(/g;
92
- let m;
93
- while ((m = callStart.exec(body))) {
94
- const args = {};
95
- let i = m.index + m[0].length;
96
- for (;;) {
97
- const key = /^\s*([A-Za-z_]\w*)\s*=\s*/.exec(body.slice(i));
98
- if (!key)
99
- break;
100
- i += key[0].length;
101
- const first = body[i];
102
- let end;
103
- if (first === '"' || first === "'") {
104
- end = stringEnd(body, i);
105
- if (end < 0)
106
- break;
107
- args[key[1]] = stringText(body.slice(i, end + 1));
108
- }
109
- else if (first === "[" || first === "{") {
110
- end = bracketEnd(body, i);
111
- if (end < 0)
112
- break;
113
- args[key[1]] = parseValue(body.slice(i, end + 1));
114
- }
115
- else {
116
- const rest = /^[^,)]*/.exec(body.slice(i))?.[0] ?? "";
117
- end = i + rest.length - 1;
118
- args[key[1]] = parseValue(rest);
119
- }
120
- i = end + 1;
121
- const comma = /^\s*,/.exec(body.slice(i));
122
- if (!comma)
123
- break;
124
- i += comma[0].length;
125
- }
126
- calls.push({ name: m[1], args });
127
- // The next call starts after this one's closing parenthesis.
128
- const close = body.indexOf(")", i);
129
- callStart.lastIndex = close < 0 ? body.length : close + 1;
130
- }
131
- return calls;
132
- }
133
- /** Every tool call in `text`. */
134
- export function parseToolCalls(text) {
135
- const calls = [];
136
- for (const m of text.matchAll(/<\|tool_call_start\|>([\s\S]*?)(?:<\|tool_call_end\|>|$)/g)) {
137
- calls.push(...parsePythonic(m[1]));
138
- }
139
- return calls;
140
- }
141
- /** Length of the longest end of `text` that could start one of `markers`. */
142
- function partialMarker(text, markers) {
143
- for (let n = Math.min(text.length, 18); n > 0; n--) {
144
- const end = text.slice(-n);
145
- if (markers.some((m) => m.startsWith(end)))
146
- return n;
147
- }
148
- return 0;
149
- }
150
- /**
151
- * Splits a streaming answer into the text before the first tool call (passed
152
- * through as it arrives) and the tool-call markup after it (held back and
153
- * parsed once the reply ends).
154
- */
155
- export function createToolCallSplitter(markers = [TOOL_CALL_START]) {
156
- let held = "";
157
- let markup = null;
158
- /** Text that can be shown now. */
159
- function push(text) {
160
- if (markup !== null) {
161
- markup += text;
162
- return "";
163
- }
164
- held += text;
165
- const at = Math.min(...markers.map((m) => held.indexOf(m)).filter((i) => i >= 0));
166
- if (Number.isFinite(at)) {
167
- const before = held.slice(0, at);
168
- markup = held.slice(at);
169
- held = "";
170
- return before;
171
- }
172
- const keep = partialMarker(held, markers);
173
- const out = held.slice(0, held.length - keep);
174
- held = held.slice(out.length);
175
- return out;
176
- }
177
- /** The rest of the text, and the tool-call markup if any. */
178
- function flush() {
179
- const rest = held;
180
- held = "";
181
- return { text: rest, markup };
182
- }
183
- return { push, flush };
184
- }
185
- /** A value written the way Python (and LFM2.5) writes it. */
186
- function pythonValue(value) {
187
- if (value === true)
188
- return "True";
189
- if (value === false)
190
- return "False";
191
- if (value === null || value === undefined)
192
- return "None";
193
- return JSON.stringify(value);
194
- }
195
- /** A past tool call in LFM2.5's own format, for the assistant turn's text. */
196
- export function formatLfm2ToolCalls(calls) {
197
- const list = calls
198
- .map((c) => `${c.name}(${Object.entries(c.args)
199
- .map(([k, v]) => `${k}=${pythonValue(v)}`)
200
- .join(", ")})`)
201
- .join(", ");
202
- return `${TOOL_CALL_START}[${list}]${TOOL_CALL_END}`;
203
- }