@tanstack-ai-browser/wllama 0.0.0-stage → 0.2.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 +151 -2
- package/dist/adapter.d.ts +166 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +183 -0
- package/dist/audio.d.ts +16 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +80 -0
- package/dist/catalog.d.ts +104 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +307 -0
- package/dist/client.d.ts +64 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +148 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/model-memory.d.ts +46 -0
- package/dist/model-memory.d.ts.map +1 -0
- package/dist/model-memory.js +79 -0
- package/dist/model-meta.d.ts +7 -0
- package/dist/model-meta.d.ts.map +1 -0
- package/dist/model-meta.js +5 -0
- package/dist/shared-load.d.ts +48 -0
- package/dist/shared-load.d.ts.map +1 -0
- package/dist/shared-load.js +103 -0
- package/dist/store.d.ts +8 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +17 -0
- package/dist/template.d.ts +26 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +25 -0
- package/dist/thinking.d.ts +39 -0
- package/dist/thinking.d.ts.map +1 -0
- package/dist/thinking.js +140 -0
- package/dist/tool-calls.d.ts +33 -0
- package/dist/tool-calls.d.ts.map +1 -0
- package/dist/tool-calls.js +203 -0
- package/package.json +62 -3
- package/src/adapter.ts +330 -0
- package/src/audio.test.ts +83 -0
- package/src/audio.ts +79 -0
- package/src/catalog.ts +399 -0
- package/src/client.ts +195 -0
- package/src/index.ts +11 -0
- package/src/runtime.test.ts +220 -0
- package/src/wllama-version.test.ts +19 -0
- package/src/wrapper.test.ts +173 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client over wllama: availability from its storage, one load per
|
|
3
|
+
* model and settings with the download's progress, a new load for other
|
|
4
|
+
* settings, and unloading. wllama is faked with the API the client uses.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict"
|
|
7
|
+
import { after, test } from "node:test"
|
|
8
|
+
import { mkdtemp, rm } from "node:fs/promises"
|
|
9
|
+
import { tmpdir } from "node:os"
|
|
10
|
+
import { join, resolve } from "node:path"
|
|
11
|
+
import { pathToFileURL } from "node:url"
|
|
12
|
+
import { createRequire } from "node:module"
|
|
13
|
+
|
|
14
|
+
import { wllamaText } from "./adapter.js"
|
|
15
|
+
import { modelUrl } from "./catalog.js"
|
|
16
|
+
|
|
17
|
+
const MODEL = "LiquidAI/LFM2.5-350M-GGUF"
|
|
18
|
+
|
|
19
|
+
test("on a server, which has no Web Worker or Origin Private File System, the model is unavailable", async () => {
|
|
20
|
+
assert.equal(typeof Worker, "undefined")
|
|
21
|
+
assert.equal(await wllamaText(MODEL).availability(), "unavailable")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const root = resolve(process.cwd(), "../..")
|
|
25
|
+
const require = createRequire(join(root, "packages/wllama/package.json"))
|
|
26
|
+
const { build } = (await import(pathToFileURL(require.resolve("esbuild")).href)) as typeof import("esbuild")
|
|
27
|
+
const temporary = await mkdtemp(join(tmpdir(), "wllama-runtime-"))
|
|
28
|
+
after(() => rm(temporary, { recursive: true, force: true }))
|
|
29
|
+
|
|
30
|
+
type Control = {
|
|
31
|
+
stored: Array<{ url: string; size: number }>
|
|
32
|
+
/** Holds each download until it resolves. */
|
|
33
|
+
gate: Promise<void>
|
|
34
|
+
/** Called once a download has reported progress. */
|
|
35
|
+
progressed: null | (() => void)
|
|
36
|
+
/** Each load: its URL and options. */
|
|
37
|
+
loads: Array<{ url: string; params: Record<string, unknown> }>
|
|
38
|
+
/** wllama instances made. */
|
|
39
|
+
instances: number
|
|
40
|
+
/** wllama instances ended. */
|
|
41
|
+
exits: number
|
|
42
|
+
/** Rejects the next loads with it. */
|
|
43
|
+
loadError?: Error
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Built = typeof import("./client.js") & Pick<typeof import("./adapter.js"), "wllamaText">
|
|
47
|
+
|
|
48
|
+
/** Builds client.ts against a fake wllama, in a scope with a Worker class and OPFS, and runs `body` with it. */
|
|
49
|
+
async function withClient(name: string, body: (client: Built, control: Control, release: () => void) => Promise<void>) {
|
|
50
|
+
const descriptors = Object.fromEntries(["navigator", "Worker"].map((key) => [key, Object.getOwnPropertyDescriptor(globalThis, key)]))
|
|
51
|
+
let release!: () => void
|
|
52
|
+
const control: Control = {
|
|
53
|
+
stored: [],
|
|
54
|
+
gate: new Promise<void>((resolve) => (release = resolve)),
|
|
55
|
+
progressed: null,
|
|
56
|
+
loads: [],
|
|
57
|
+
instances: 0,
|
|
58
|
+
exits: 0
|
|
59
|
+
}
|
|
60
|
+
;(globalThis as { __wllamaRuntime?: Control }).__wllamaRuntime = control
|
|
61
|
+
const define = (key: string, value: unknown) => Object.defineProperty(globalThis, key, { configurable: true, writable: true, value })
|
|
62
|
+
define("navigator", { storage: { getDirectory: async () => ({}) } })
|
|
63
|
+
define("Worker", class {})
|
|
64
|
+
try {
|
|
65
|
+
const outfile = join(temporary, `${name}.mjs`)
|
|
66
|
+
await build({
|
|
67
|
+
stdin: { contents: 'export * from "./client.ts"; export { wllamaText } from "./adapter.ts"', resolveDir: join(root, "packages/wllama/src"), loader: "ts" },
|
|
68
|
+
outfile,
|
|
69
|
+
bundle: true,
|
|
70
|
+
format: "esm",
|
|
71
|
+
platform: "node",
|
|
72
|
+
logLevel: "error",
|
|
73
|
+
plugins: [
|
|
74
|
+
{
|
|
75
|
+
name: "fake-wllama",
|
|
76
|
+
setup(bundle) {
|
|
77
|
+
bundle.onResolve({ filter: /@wllama\/wllama\/esm\/index\.js$/ }, () => ({ path: "wllama", namespace: "fake" }))
|
|
78
|
+
bundle.onLoad({ filter: /.*/, namespace: "fake" }, () => ({
|
|
79
|
+
contents: `
|
|
80
|
+
const control=globalThis.__wllamaRuntime;
|
|
81
|
+
export const LoggerWithoutDebug={};
|
|
82
|
+
export class ModelManager { async getModels(){ return control.stored } }
|
|
83
|
+
export class Wllama {
|
|
84
|
+
constructor(){control.instances++}
|
|
85
|
+
setCompat(){}
|
|
86
|
+
async loadModelFromUrl(url,params){
|
|
87
|
+
control.loads.push({url,params});
|
|
88
|
+
if(control.loadError) throw control.loadError;
|
|
89
|
+
const stored=control.stored.find((m)=>m.url===url);
|
|
90
|
+
if(stored){ params.progressCallback({loaded:stored.size,total:stored.size}); return }
|
|
91
|
+
params.progressCallback({loaded:250,total:1000});
|
|
92
|
+
control.progressed?.();
|
|
93
|
+
await Promise.race([control.gate, new Promise((r)=>params.signal.addEventListener("abort",r))]);
|
|
94
|
+
params.signal.throwIfAborted();
|
|
95
|
+
params.progressCallback({loaded:1000,total:1000});
|
|
96
|
+
control.stored.push({url,size:1000});
|
|
97
|
+
}
|
|
98
|
+
async exit(){control.exits++}
|
|
99
|
+
}
|
|
100
|
+
`
|
|
101
|
+
}))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
})
|
|
106
|
+
await body((await import(pathToFileURL(outfile).href)) as Built, control, release)
|
|
107
|
+
} finally {
|
|
108
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
109
|
+
if (descriptor) Object.defineProperty(globalThis, key, descriptor)
|
|
110
|
+
else Reflect.deleteProperty(globalThis, key)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
test("availability: downloadable, downloading during the download, available once loaded and from storage after unloading", { timeout: 5000 }, async () => {
|
|
116
|
+
await withClient("states", async (client, control, release) => {
|
|
117
|
+
assert.equal(await client.availability(MODEL), "downloadable")
|
|
118
|
+
const progress: Array<number> = []
|
|
119
|
+
const progressed = new Promise<void>((resolve) => (control.progressed = resolve))
|
|
120
|
+
const loading = client.loadModel(MODEL, {}, { onDownloadProgress: (value) => progress.push(value) })
|
|
121
|
+
await progressed
|
|
122
|
+
assert.equal(await client.availability(MODEL), "downloading")
|
|
123
|
+
release()
|
|
124
|
+
await loading
|
|
125
|
+
assert.deepEqual(progress, [0.25, 1], "the download's progress, then 1 once ready")
|
|
126
|
+
assert.equal(await client.availability(MODEL), "available")
|
|
127
|
+
await client.unloadModel(MODEL)
|
|
128
|
+
assert.equal(control.exits, 1)
|
|
129
|
+
assert.equal(await client.availability(MODEL), "available", "its file stays in storage")
|
|
130
|
+
control.stored = []
|
|
131
|
+
assert.equal(await client.availability(MODEL), "downloadable")
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test("a model in storage loads without download progress: just 1", { timeout: 5000 }, async () => {
|
|
136
|
+
await withClient("stored", async (client, control) => {
|
|
137
|
+
control.stored.push({ url: modelUrl(MODEL), size: 1000 })
|
|
138
|
+
const progress: Array<number> = []
|
|
139
|
+
await client.loadModel(MODEL, {}, { onDownloadProgress: (value) => progress.push(value) })
|
|
140
|
+
assert.deepEqual(progress, [1])
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test("the adapter loads with its own settings; the same settings share a load, other settings load again", { timeout: 5000 }, async () => {
|
|
145
|
+
await withClient("settings", async (client, control, release) => {
|
|
146
|
+
release()
|
|
147
|
+
await client.wllamaText(MODEL, { contextLength: 2048, gpu: false, threads: 4 }).create()
|
|
148
|
+
assert.equal(control.loads.at(-1)?.url, modelUrl(MODEL))
|
|
149
|
+
assert.equal(control.loads.at(-1)?.params.n_ctx, 2048)
|
|
150
|
+
assert.equal(control.loads.at(-1)?.params.n_gpu_layers, 0)
|
|
151
|
+
assert.equal(control.loads.at(-1)?.params.n_threads, 4)
|
|
152
|
+
await client.wllamaText(MODEL, { contextLength: 2048, gpu: false, threads: 4 }).create()
|
|
153
|
+
assert.equal(control.loads.length, 1, "the same settings: no new load")
|
|
154
|
+
await client.wllamaText(MODEL).create()
|
|
155
|
+
assert.equal(control.loads.length, 2, "other settings load it again")
|
|
156
|
+
assert.equal(control.loads.at(-1)?.params.n_ctx, 0, "the model's training context by default")
|
|
157
|
+
assert.equal(control.exits, 1, "the previous load was freed")
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test("aborting a wait stops only that caller; unloading stops the download", { timeout: 5000 }, async () => {
|
|
162
|
+
await withClient("abort", async (client, control) => {
|
|
163
|
+
const controller = new AbortController()
|
|
164
|
+
const waiting = client.loadModel(MODEL, {}, { signal: controller.signal })
|
|
165
|
+
controller.abort()
|
|
166
|
+
await assert.rejects(waiting)
|
|
167
|
+
assert.equal(control.loads.length, 1, "the load goes on")
|
|
168
|
+
const again = client.loadModel(MODEL, {})
|
|
169
|
+
await client.unloadModel()
|
|
170
|
+
await assert.rejects(again)
|
|
171
|
+
assert.equal(await client.availability(MODEL), "downloadable")
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test("a failed load rejects, frees wllama, and the next call loads again", { timeout: 5000 }, async () => {
|
|
176
|
+
await withClient("failure", async (client, control, release) => {
|
|
177
|
+
release()
|
|
178
|
+
control.loadError = new Error("llama.cpp couldn't load it")
|
|
179
|
+
await assert.rejects(client.wllamaText(MODEL).create(), /couldn't load/)
|
|
180
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
181
|
+
assert.equal(control.exits, 1)
|
|
182
|
+
control.loadError = undefined
|
|
183
|
+
await client.wllamaText(MODEL).create()
|
|
184
|
+
assert.equal(control.loads.length, 2)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test("an unlisted model is named by its GGUF file: owner/repo/file.gguf on Hugging Face, or a URL", { timeout: 5000 }, async () => {
|
|
189
|
+
await withClient("unlisted", async (client, control, release) => {
|
|
190
|
+
release()
|
|
191
|
+
await client.wllamaText("bartowski/SmolLM2-135M-Instruct-GGUF/SmolLM2-135M-Instruct-Q4_K_M.gguf").create()
|
|
192
|
+
assert.equal(control.loads.at(-1)?.url, "https://huggingface.co/bartowski/SmolLM2-135M-Instruct-GGUF/resolve/main/SmolLM2-135M-Instruct-Q4_K_M.gguf")
|
|
193
|
+
await client.wllamaText("https://example.com/m.gguf").create()
|
|
194
|
+
assert.equal(control.loads.at(-1)?.url, "https://example.com/m.gguf")
|
|
195
|
+
await assert.rejects(client.wllamaText("some/repo").create(), /name its GGUF file/)
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test("simultaneous cold starts with the same settings share one load, which unloading frees", { timeout: 5000 }, async () => {
|
|
200
|
+
await withClient("simultaneous", async (client, control) => {
|
|
201
|
+
control.stored.push({ url: modelUrl(MODEL), size: 1000 })
|
|
202
|
+
const [first, second] = await Promise.all([client.loadModel(MODEL, {}), client.loadModel(MODEL, {})])
|
|
203
|
+
assert.equal(control.loads.length, 1)
|
|
204
|
+
assert.equal(first, second)
|
|
205
|
+
await client.unloadModel()
|
|
206
|
+
assert.equal(control.instances, 1)
|
|
207
|
+
assert.equal(control.exits, control.instances, "no wllama is left running")
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test("unloading during a load from storage stops it: the load rejects and no wllama is left running", { timeout: 5000 }, async () => {
|
|
212
|
+
await withClient("unload-stored", async (client, control) => {
|
|
213
|
+
control.stored.push({ url: modelUrl(MODEL), size: 1000 })
|
|
214
|
+
const loading = client.loadModel(MODEL, {})
|
|
215
|
+
await client.unloadModel()
|
|
216
|
+
await assert.rejects(loading)
|
|
217
|
+
assert.equal(control.exits, control.instances, "no wllama is left running")
|
|
218
|
+
assert.equal(await client.availability(MODEL), "available", "its file stays in storage")
|
|
219
|
+
})
|
|
220
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { test } from "node:test"
|
|
3
|
+
import { readFileSync } from "node:fs"
|
|
4
|
+
import { createRequire } from "node:module"
|
|
5
|
+
import { join, resolve } from "node:path"
|
|
6
|
+
|
|
7
|
+
const pkg = resolve(process.cwd(), "../../packages/wllama")
|
|
8
|
+
const require = createRequire(join(pkg, "package.json"))
|
|
9
|
+
|
|
10
|
+
test("the default WebAssembly URL is the installed @wllama/wllama's version", () => {
|
|
11
|
+
const installed = (require("@wllama/wllama/package.json") as { version: string }).version
|
|
12
|
+
const source = readFileSync(join(pkg, "src/client.ts"), "utf8")
|
|
13
|
+
const pinned = /export const WLLAMA_VERSION = "([^"]+)"/.exec(source)?.[1]
|
|
14
|
+
assert.equal(pinned, installed, "update WLLAMA_VERSION in client.ts with the dependency")
|
|
15
|
+
// The file's path in the package has moved between versions (src/wasm in 3.8, esm/wasm in 3.13).
|
|
16
|
+
const url = /export const WLLAMA_WASM_URL = `([^`]+)`/.exec(source)?.[1].replace("${WLLAMA_VERSION}", installed)
|
|
17
|
+
const own = (require("@wllama/wllama/esm/wasm-from-cdn.cjs") as { default: { default: string } }).default.default
|
|
18
|
+
assert.equal(url, own, "WLLAMA_WASM_URL is wllama's own WasmFromCDN URL")
|
|
19
|
+
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the adapter adds to TanStack's Chat Completions adapter: llama.cpp's
|
|
3
|
+
* request fields over the model card's sampling, the chat template's thinking
|
|
4
|
+
* switch, reading thinking, schemas as given, and the caller's signal
|
|
5
|
+
* reaching wllama. llama.cpp is stubbed.
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict"
|
|
8
|
+
import { after, beforeEach, test } from "node:test"
|
|
9
|
+
import { mkdtemp, rm } from "node:fs/promises"
|
|
10
|
+
import { tmpdir } from "node:os"
|
|
11
|
+
import { join, resolve } from "node:path"
|
|
12
|
+
import { pathToFileURL } from "node:url"
|
|
13
|
+
import { createRequire } from "node:module"
|
|
14
|
+
|
|
15
|
+
const root = resolve(process.cwd(), "../..")
|
|
16
|
+
const require = createRequire(join(root, "packages/wllama/package.json"))
|
|
17
|
+
const { build } = (await import(pathToFileURL(require.resolve("esbuild")).href)) as typeof import("esbuild")
|
|
18
|
+
const temporary = await mkdtemp(join(tmpdir(), "wllama-wrapper-"))
|
|
19
|
+
after(() => rm(temporary, { recursive: true, force: true }))
|
|
20
|
+
|
|
21
|
+
/** What the stubbed llama.cpp receives, and the chunks it streams back. */
|
|
22
|
+
const control = {
|
|
23
|
+
requests: [] as Array<{ request: Record<string, unknown>; signal?: AbortSignal }>,
|
|
24
|
+
chunks: [] as Array<unknown>
|
|
25
|
+
}
|
|
26
|
+
;(globalThis as { __wllamaWrapper?: typeof control }).__wllamaWrapper = control
|
|
27
|
+
|
|
28
|
+
const outfile = join(temporary, "wllama-adapter.mjs")
|
|
29
|
+
await build({
|
|
30
|
+
entryPoints: [join(root, "packages/wllama/src/adapter.ts")],
|
|
31
|
+
outfile,
|
|
32
|
+
bundle: true,
|
|
33
|
+
format: "esm",
|
|
34
|
+
platform: "node",
|
|
35
|
+
conditions: ["@tanstack-ai-browser/source"],
|
|
36
|
+
logLevel: "error",
|
|
37
|
+
plugins: [
|
|
38
|
+
{
|
|
39
|
+
name: "stub-llama-cpp",
|
|
40
|
+
setup(bundle) {
|
|
41
|
+
bundle.onResolve({ filter: /^\.\/client\.js$/ }, (args) =>
|
|
42
|
+
args.importer.endsWith("/wllama/src/adapter.ts") ? { path: "client", namespace: "stub" } : undefined
|
|
43
|
+
)
|
|
44
|
+
bundle.onLoad({ filter: /.*/, namespace: "stub" }, () => ({
|
|
45
|
+
contents: `
|
|
46
|
+
const control=globalThis.__wllamaWrapper;
|
|
47
|
+
export const availability=async()=>"available";export const unloadModel=async()=>{};
|
|
48
|
+
export const loadModel=async()=>({
|
|
49
|
+
createChatCompletion:async({abortSignal,...request})=>{
|
|
50
|
+
control.requests.push({request,signal:abortSignal});
|
|
51
|
+
if(request.stream) return (async function*(){yield* control.chunks})();
|
|
52
|
+
return {choices:[{index:0,finish_reason:"stop",message:{role:"assistant",content:'{"city":"Oslo"}'}}]};
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
`
|
|
56
|
+
}))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
type Chunk = { type: string; [key: string]: unknown }
|
|
63
|
+
type Adapter = {
|
|
64
|
+
model: string
|
|
65
|
+
chatStream(options: Record<string, unknown>): AsyncIterable<Chunk>
|
|
66
|
+
structuredOutput(options: Record<string, unknown>): Promise<{ data: unknown }>
|
|
67
|
+
}
|
|
68
|
+
const { wllamaText } = (await import(pathToFileURL(outfile).href)) as { wllamaText: (model: string) => Adapter }
|
|
69
|
+
|
|
70
|
+
const MODEL = "unsloth/Qwen3.5-2B-GGUF"
|
|
71
|
+
const logger = { request() { return undefined }, errors() { return undefined }, provider() { return undefined }, warn() { return undefined } }
|
|
72
|
+
const options = (extra: Record<string, unknown> = {}) => ({
|
|
73
|
+
logger,
|
|
74
|
+
model: MODEL,
|
|
75
|
+
threadId: "wrapper",
|
|
76
|
+
messages: [{ role: "user", content: "hi" }],
|
|
77
|
+
...extra
|
|
78
|
+
})
|
|
79
|
+
const delta = (d: object, finish: string | null = null) => ({ id: "c", object: "chat.completion.chunk", created: 0, model: MODEL, choices: [{ index: 0, delta: d, finish_reason: finish }] })
|
|
80
|
+
|
|
81
|
+
async function run(extra: Record<string, unknown> = {}) {
|
|
82
|
+
const events: Array<Chunk> = []
|
|
83
|
+
for await (const event of wllamaText(MODEL).chatStream(options(extra))) events.push(event)
|
|
84
|
+
return { events, request: control.requests.at(-1)!.request }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
control.requests = []
|
|
89
|
+
control.chunks = [delta({ role: "assistant", content: "Hi" }), delta({}, "stop")]
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test("modelOptions go to llama.cpp as given, over the model card's sampling", async () => {
|
|
93
|
+
const given = { max_tokens: 64, temperature: 0.2, top_p: 0.9, min_p: 0.05, frequency_penalty: 0.1, presence_penalty: 0.5, seed: 7, stop: ["END"] }
|
|
94
|
+
const { request } = await run({ modelOptions: given })
|
|
95
|
+
for (const [key, value] of Object.entries(given)) assert.deepEqual(request[key], value, key)
|
|
96
|
+
// Left out: the model card's.
|
|
97
|
+
const card = (await run()).request
|
|
98
|
+
assert.equal(typeof card.top_k, "number")
|
|
99
|
+
assert.equal(typeof card.repeat_penalty, "number")
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test("Qwen3.5's card sampling: its non-thinking settings by default, its thinking settings while thinking", async () => {
|
|
103
|
+
const plain = (await run()).request
|
|
104
|
+
assert.deepEqual(
|
|
105
|
+
[plain.temperature, plain.top_p, plain.top_k, plain.min_p, plain.presence_penalty, plain.repeat_penalty],
|
|
106
|
+
[1, 1, 20, 0, 2, 1]
|
|
107
|
+
)
|
|
108
|
+
const thinking = (await run({ reasoning: { level: "high", summary: false } })).request
|
|
109
|
+
assert.deepEqual([thinking.top_p, thinking.presence_penalty], [0.95, 1.5])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test("parallel_tool_calls goes to llama.cpp as given; the request asks for usage", async () => {
|
|
113
|
+
const tools = [{ name: "lookup", description: "Look up", inputSchema: { type: "object", properties: { q: { type: "string" } } } }]
|
|
114
|
+
const request = (await run({ tools, modelOptions: { parallel_tool_calls: false } })).request
|
|
115
|
+
assert.equal(request.parallel_tool_calls, false)
|
|
116
|
+
assert.deepEqual(request.stream_options, { include_usage: true })
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test("reasoning: the chat template's enable_thinking, a budget when thinking, nothing when not asked", async () => {
|
|
120
|
+
const off = (await run({ reasoning: { level: "off", summary: false } })).request
|
|
121
|
+
assert.deepEqual(off.chat_template_kwargs, { enable_thinking: false })
|
|
122
|
+
assert.equal(off.reasoning_effort, undefined)
|
|
123
|
+
const high = (await run({ reasoning: { level: "high", summary: false, budgetTokens: 256 } })).request
|
|
124
|
+
assert.deepEqual(high.chat_template_kwargs, { enable_thinking: true })
|
|
125
|
+
assert.equal(high.reasoning_budget_tokens, 256)
|
|
126
|
+
assert.equal((await run()).request.chat_template_kwargs, undefined)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test("thinking streamed as reasoning_content comes back as REASONING events", async () => {
|
|
130
|
+
control.chunks = [delta({ role: "assistant", reasoning_content: "Let me think." }), delta({ content: "Hi" }), delta({}, "stop")]
|
|
131
|
+
const { events } = await run()
|
|
132
|
+
assert.equal(events.find((e) => e.type === "REASONING_MESSAGE_CONTENT")?.delta, "Let me think.")
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test("structured output sends the schema as given; the caller's signal reaches wllama", async () => {
|
|
136
|
+
const schema = { type: "object", properties: { city: { type: "string" }, note: { type: "string" } }, required: ["city"] }
|
|
137
|
+
const controller = new AbortController()
|
|
138
|
+
const result = await wllamaText(MODEL).structuredOutput({
|
|
139
|
+
chatOptions: options({ request: { signal: controller.signal } }),
|
|
140
|
+
outputSchema: schema
|
|
141
|
+
})
|
|
142
|
+
assert.deepEqual(result.data, { city: "Oslo" })
|
|
143
|
+
const sent = control.requests.at(-1)!
|
|
144
|
+
assert.deepEqual((sent.request.response_format as { json_schema: { schema: unknown } }).json_schema.schema, schema)
|
|
145
|
+
assert.equal(sent.signal, controller.signal)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test("an audio clip and an image reach llama.cpp as their bytes, in their places", async () => {
|
|
149
|
+
// A WAV file's first bytes, and three of an image.
|
|
150
|
+
const wav = Uint8Array.from([..."RIFF", 0, 0, 0, 0, ..."WAVE", 7, 8, 9].map((v) => (typeof v === "string" ? v.charCodeAt(0) : v)))
|
|
151
|
+
const data = (bytes: Uint8Array) => Buffer.from(bytes).toString("base64")
|
|
152
|
+
const content = [
|
|
153
|
+
{ type: "audio", source: { type: "data", value: data(wav), mimeType: "audio/wav" } },
|
|
154
|
+
{ type: "text", content: "What is said here, and what is in the picture?" },
|
|
155
|
+
{ type: "image", source: { type: "data", value: data(Uint8Array.from([1, 2, 3])), mimeType: "image/png" } }
|
|
156
|
+
]
|
|
157
|
+
const { request } = await run({ messages: [{ role: "user", content }] })
|
|
158
|
+
const messages = request.messages as Array<{ content: Array<{ type: string; data?: ArrayBuffer }> }>
|
|
159
|
+
const parts = messages.at(-1)?.content ?? []
|
|
160
|
+
assert.deepEqual(parts.map((part) => part.type), ["audio", "text", "image"])
|
|
161
|
+
const [audio, , image] = parts
|
|
162
|
+
assert.deepEqual(new Uint8Array(audio?.data ?? new ArrayBuffer(0)), wav)
|
|
163
|
+
assert.deepEqual([...new Uint8Array(image?.data ?? new ArrayBuffer(0))], [1, 2, 3])
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test("audio kept as an uploaded file is refused: wllama reads bytes", async () => {
|
|
167
|
+
const content = [{ type: "audio", source: { type: "file", value: "file-123" } }]
|
|
168
|
+
const events: Array<Chunk> = []
|
|
169
|
+
for await (const event of wllamaText(MODEL).chatStream(options({ messages: [{ role: "user", content }] }))) events.push(event)
|
|
170
|
+
const failure = events.find((event) => event.type === "RUN_ERROR")
|
|
171
|
+
assert.match(JSON.stringify(failure), /not as uploaded files/)
|
|
172
|
+
assert.equal(control.requests.length, 0)
|
|
173
|
+
})
|