@browser-ai/web-llm 1.0.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/dist/index.mjs ADDED
@@ -0,0 +1,1343 @@
1
+ // ../../../node_modules/@ai-sdk/provider/dist/index.mjs
2
+ var marker = "vercel.ai.error";
3
+ var symbol = Symbol.for(marker);
4
+ var _a;
5
+ var _AISDKError = class _AISDKError2 extends Error {
6
+ /**
7
+ * Creates an AI SDK Error.
8
+ *
9
+ * @param {Object} params - The parameters for creating the error.
10
+ * @param {string} params.name - The name of the error.
11
+ * @param {string} params.message - The error message.
12
+ * @param {unknown} [params.cause] - The underlying cause of the error.
13
+ */
14
+ constructor({
15
+ name: name14,
16
+ message,
17
+ cause
18
+ }) {
19
+ super(message);
20
+ this[_a] = true;
21
+ this.name = name14;
22
+ this.cause = cause;
23
+ }
24
+ /**
25
+ * Checks if the given error is an AI SDK Error.
26
+ * @param {unknown} error - The error to check.
27
+ * @returns {boolean} True if the error is an AI SDK Error, false otherwise.
28
+ */
29
+ static isInstance(error) {
30
+ return _AISDKError2.hasMarker(error, marker);
31
+ }
32
+ static hasMarker(error, marker15) {
33
+ const markerSymbol = Symbol.for(marker15);
34
+ return error != null && typeof error === "object" && markerSymbol in error && typeof error[markerSymbol] === "boolean" && error[markerSymbol] === true;
35
+ }
36
+ };
37
+ _a = symbol;
38
+ var AISDKError = _AISDKError;
39
+ var name = "AI_APICallError";
40
+ var marker2 = `vercel.ai.error.${name}`;
41
+ var symbol2 = Symbol.for(marker2);
42
+ var _a2;
43
+ _a2 = symbol2;
44
+ var name2 = "AI_EmptyResponseBodyError";
45
+ var marker3 = `vercel.ai.error.${name2}`;
46
+ var symbol3 = Symbol.for(marker3);
47
+ var _a3;
48
+ _a3 = symbol3;
49
+ var name3 = "AI_InvalidArgumentError";
50
+ var marker4 = `vercel.ai.error.${name3}`;
51
+ var symbol4 = Symbol.for(marker4);
52
+ var _a4;
53
+ _a4 = symbol4;
54
+ var name4 = "AI_InvalidPromptError";
55
+ var marker5 = `vercel.ai.error.${name4}`;
56
+ var symbol5 = Symbol.for(marker5);
57
+ var _a5;
58
+ _a5 = symbol5;
59
+ var name5 = "AI_InvalidResponseDataError";
60
+ var marker6 = `vercel.ai.error.${name5}`;
61
+ var symbol6 = Symbol.for(marker6);
62
+ var _a6;
63
+ _a6 = symbol6;
64
+ var name6 = "AI_JSONParseError";
65
+ var marker7 = `vercel.ai.error.${name6}`;
66
+ var symbol7 = Symbol.for(marker7);
67
+ var _a7;
68
+ _a7 = symbol7;
69
+ var name7 = "AI_LoadAPIKeyError";
70
+ var marker8 = `vercel.ai.error.${name7}`;
71
+ var symbol8 = Symbol.for(marker8);
72
+ var _a8;
73
+ _a8 = symbol8;
74
+ var name8 = "AI_LoadSettingError";
75
+ var marker9 = `vercel.ai.error.${name8}`;
76
+ var symbol9 = Symbol.for(marker9);
77
+ var _a9;
78
+ var LoadSettingError = class extends AISDKError {
79
+ // used in isInstance
80
+ constructor({ message }) {
81
+ super({ name: name8, message });
82
+ this[_a9] = true;
83
+ }
84
+ static isInstance(error) {
85
+ return AISDKError.hasMarker(error, marker9);
86
+ }
87
+ };
88
+ _a9 = symbol9;
89
+ var name9 = "AI_NoContentGeneratedError";
90
+ var marker10 = `vercel.ai.error.${name9}`;
91
+ var symbol10 = Symbol.for(marker10);
92
+ var _a10;
93
+ _a10 = symbol10;
94
+ var name10 = "AI_NoSuchModelError";
95
+ var marker11 = `vercel.ai.error.${name10}`;
96
+ var symbol11 = Symbol.for(marker11);
97
+ var _a11;
98
+ _a11 = symbol11;
99
+ var name11 = "AI_TooManyEmbeddingValuesForCallError";
100
+ var marker12 = `vercel.ai.error.${name11}`;
101
+ var symbol12 = Symbol.for(marker12);
102
+ var _a12;
103
+ _a12 = symbol12;
104
+ var name12 = "AI_TypeValidationError";
105
+ var marker13 = `vercel.ai.error.${name12}`;
106
+ var symbol13 = Symbol.for(marker13);
107
+ var _a13;
108
+ _a13 = symbol13;
109
+ var name13 = "AI_UnsupportedFunctionalityError";
110
+ var marker14 = `vercel.ai.error.${name13}`;
111
+ var symbol14 = Symbol.for(marker14);
112
+ var _a14;
113
+ var UnsupportedFunctionalityError = class extends AISDKError {
114
+ constructor({
115
+ functionality,
116
+ message = `'${functionality}' functionality not supported.`
117
+ }) {
118
+ super({ name: name13, message });
119
+ this[_a14] = true;
120
+ this.functionality = functionality;
121
+ }
122
+ static isInstance(error) {
123
+ return AISDKError.hasMarker(error, marker14);
124
+ }
125
+ };
126
+ _a14 = symbol14;
127
+
128
+ // src/tool-calling/build-json-system-prompt.ts
129
+ function buildJsonToolSystemPrompt(originalSystemPrompt, tools, options) {
130
+ if (!tools || tools.length === 0) {
131
+ return originalSystemPrompt || "";
132
+ }
133
+ const parallelInstruction = "Only request one tool call at a time. Wait for tool results before asking for another tool.";
134
+ const toolSchemas = tools.map((tool) => {
135
+ const schema = getParameters(tool);
136
+ return {
137
+ name: tool.name,
138
+ description: tool.description ?? "No description provided.",
139
+ parameters: schema || { type: "object", properties: {} }
140
+ };
141
+ });
142
+ const toolsJson = JSON.stringify(toolSchemas, null, 2);
143
+ const instructionBody = `You are a helpful AI assistant with access to tools.
144
+
145
+ # Available Tools
146
+ ${toolsJson}
147
+
148
+ # Tool Calling Instructions
149
+ ${parallelInstruction}
150
+
151
+ To call a tool, output JSON in this exact format inside a \`\`\`tool_call code fence:
152
+
153
+ \`\`\`tool_call
154
+ {"name": "tool_name", "arguments": {"param1": "value1", "param2": "value2"}}
155
+ \`\`\`
156
+
157
+ Tool responses will be provided in \`\`\`tool_result fences. Each line contains JSON like:
158
+ \`\`\`tool_result
159
+ {"id": "call_123", "name": "tool_name", "result": {...}, "error": false}
160
+ \`\`\`
161
+ Use the \`result\` payload (and treat \`error\` as a boolean flag) when continuing the conversation.
162
+
163
+ Important:
164
+ - Use exact tool and parameter names from the schema above
165
+ - Arguments must be a valid JSON object matching the tool's parameters
166
+ - You can include brief reasoning before or after the tool call
167
+ - If no tool is needed, respond directly without tool_call fences`;
168
+ if (originalSystemPrompt?.trim()) {
169
+ return `${originalSystemPrompt.trim()}
170
+
171
+ ${instructionBody}`;
172
+ }
173
+ return instructionBody;
174
+ }
175
+ function getParameters(tool) {
176
+ if ("parameters" in tool) {
177
+ return tool.parameters;
178
+ }
179
+ return tool.inputSchema;
180
+ }
181
+
182
+ // src/tool-calling/parse-json-function-calls.ts
183
+ var JSON_TOOL_CALL_FENCE_REGEX = /```tool[_-]?call\s*([\s\S]*?)```/gi;
184
+ function generateToolCallId() {
185
+ return `call_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
186
+ }
187
+ function parseJsonFunctionCalls(response) {
188
+ const matches = Array.from(response.matchAll(JSON_TOOL_CALL_FENCE_REGEX));
189
+ JSON_TOOL_CALL_FENCE_REGEX.lastIndex = 0;
190
+ if (matches.length === 0) {
191
+ return { toolCalls: [], textContent: response };
192
+ }
193
+ const toolCalls = [];
194
+ let textContent = response;
195
+ for (const match of matches) {
196
+ const [fullFence, innerContent] = match;
197
+ textContent = textContent.replace(fullFence, "");
198
+ try {
199
+ const trimmed = innerContent.trim();
200
+ try {
201
+ const parsed = JSON.parse(trimmed);
202
+ const callsArray = Array.isArray(parsed) ? parsed : [parsed];
203
+ for (const call of callsArray) {
204
+ if (!call.name) continue;
205
+ toolCalls.push({
206
+ type: "tool-call",
207
+ toolCallId: call.id || generateToolCallId(),
208
+ toolName: call.name,
209
+ args: call.arguments || {}
210
+ });
211
+ }
212
+ } catch {
213
+ const lines = trimmed.split("\n").filter((line) => line.trim());
214
+ for (const line of lines) {
215
+ try {
216
+ const call = JSON.parse(line.trim());
217
+ if (!call.name) continue;
218
+ toolCalls.push({
219
+ type: "tool-call",
220
+ toolCallId: call.id || generateToolCallId(),
221
+ toolName: call.name,
222
+ args: call.arguments || {}
223
+ });
224
+ } catch {
225
+ continue;
226
+ }
227
+ }
228
+ }
229
+ } catch (error) {
230
+ console.warn("Failed to parse JSON tool call:", error);
231
+ continue;
232
+ }
233
+ }
234
+ textContent = textContent.replace(/\n{2,}/g, "\n");
235
+ return { toolCalls, textContent: textContent.trim() };
236
+ }
237
+
238
+ // src/tool-calling/format-tool-results.ts
239
+ function formatToolResults(results) {
240
+ if (results.length === 0) {
241
+ return "";
242
+ }
243
+ const lines = results.map((result) => formatSingleToolResult(result)).join("\n");
244
+ return `\`\`\`tool_result
245
+ ${lines}
246
+ \`\`\``;
247
+ }
248
+ function formatSingleToolResult(result) {
249
+ return JSON.stringify({
250
+ id: result.toolCallId,
251
+ name: result.toolName,
252
+ result: result.result,
253
+ error: result.isError ?? false
254
+ });
255
+ }
256
+
257
+ // src/convert-to-webllm-messages.tsx
258
+ function convertToolResultOutput(output) {
259
+ switch (output.type) {
260
+ case "text":
261
+ return { value: output.value, isError: false };
262
+ case "json":
263
+ return { value: output.value, isError: false };
264
+ case "error-text":
265
+ return { value: output.value, isError: true };
266
+ case "error-json":
267
+ return { value: output.value, isError: true };
268
+ case "content":
269
+ return { value: output.value, isError: false };
270
+ default: {
271
+ const exhaustiveCheck = output;
272
+ return { value: exhaustiveCheck, isError: false };
273
+ }
274
+ }
275
+ }
276
+ function toToolResult(part) {
277
+ const { value, isError } = convertToolResultOutput(part.output);
278
+ return {
279
+ toolCallId: part.toolCallId,
280
+ toolName: part.toolName,
281
+ result: value,
282
+ isError
283
+ };
284
+ }
285
+ function uint8ArrayToBase64(uint8array) {
286
+ const binary = Array.from(
287
+ uint8array,
288
+ (byte) => String.fromCharCode(byte)
289
+ ).join("");
290
+ return btoa(binary);
291
+ }
292
+ function convertDataToURL(data, mediaType) {
293
+ if (data instanceof URL) {
294
+ return data.toString();
295
+ }
296
+ if (typeof data === "string") {
297
+ return `data:${mediaType};base64,${data}`;
298
+ }
299
+ if (data instanceof Uint8Array) {
300
+ return `data:${mediaType};base64,${uint8ArrayToBase64(data)}`;
301
+ }
302
+ if (data instanceof ArrayBuffer) {
303
+ return `data:${mediaType};base64,${uint8ArrayToBase64(
304
+ new Uint8Array(data)
305
+ )}`;
306
+ }
307
+ if (typeof Buffer !== "undefined" && data instanceof Buffer) {
308
+ return `data:${mediaType};base64,${data.toString("base64")}`;
309
+ }
310
+ throw new UnsupportedFunctionalityError({
311
+ functionality: `file data type: ${typeof data}`
312
+ });
313
+ }
314
+ function convertToWebLLMMessages(prompt) {
315
+ const messages = [];
316
+ for (const message of prompt) {
317
+ switch (message.role) {
318
+ case "system":
319
+ messages.push({
320
+ role: "system",
321
+ content: message.content
322
+ });
323
+ break;
324
+ case "user":
325
+ const hasFileContent = message.content.some(
326
+ (part) => part.type === "file"
327
+ );
328
+ if (!hasFileContent) {
329
+ const userContent = [];
330
+ for (const part of message.content) {
331
+ if (part.type === "text") {
332
+ userContent.push(part.text);
333
+ }
334
+ }
335
+ messages.push({
336
+ role: "user",
337
+ content: userContent.join("\n")
338
+ });
339
+ break;
340
+ }
341
+ const content = [];
342
+ for (const part of message.content) {
343
+ if (part.type === "text") {
344
+ content.push({ type: "text", text: part.text });
345
+ } else if (part.type === "file") {
346
+ if (!part.mediaType?.startsWith("image/")) {
347
+ throw new UnsupportedFunctionalityError({
348
+ functionality: `file input with media type '${part.mediaType}'`
349
+ });
350
+ }
351
+ content.push({
352
+ type: "image_url",
353
+ image_url: {
354
+ url: convertDataToURL(part.data, part.mediaType)
355
+ }
356
+ });
357
+ }
358
+ }
359
+ messages.push({ role: "user", content });
360
+ break;
361
+ case "assistant":
362
+ let assistantContent = "";
363
+ const toolCallsInMessage = [];
364
+ for (const part of message.content) {
365
+ if (part.type === "text") {
366
+ assistantContent += part.text;
367
+ } else if (part.type === "tool-call") {
368
+ toolCallsInMessage.push({
369
+ toolCallId: part.toolCallId,
370
+ toolName: part.toolName
371
+ });
372
+ }
373
+ }
374
+ if (assistantContent) {
375
+ messages.push({
376
+ role: "assistant",
377
+ content: assistantContent
378
+ });
379
+ }
380
+ break;
381
+ case "tool":
382
+ const toolResults = message.content.map(toToolResult);
383
+ const formattedResults = formatToolResults(toolResults);
384
+ messages.push({
385
+ role: "user",
386
+ content: formattedResults
387
+ });
388
+ break;
389
+ }
390
+ }
391
+ return messages;
392
+ }
393
+
394
+ // src/web-llm-language-model.ts
395
+ import {
396
+ CreateWebWorkerMLCEngine,
397
+ MLCEngine
398
+ } from "@mlc-ai/web-llm";
399
+
400
+ // src/utils/warnings.ts
401
+ function createUnsupportedSettingWarning(setting, details) {
402
+ return {
403
+ type: "unsupported-setting",
404
+ setting,
405
+ details
406
+ };
407
+ }
408
+ function createUnsupportedToolWarning(tool, details) {
409
+ return {
410
+ type: "unsupported-tool",
411
+ tool,
412
+ details
413
+ };
414
+ }
415
+
416
+ // src/utils/tool-utils.ts
417
+ function isFunctionTool(tool) {
418
+ return tool.type === "function";
419
+ }
420
+
421
+ // src/utils/prompt-utils.ts
422
+ function extractSystemPrompt(messages) {
423
+ const systemMessages = messages.filter((msg) => msg.role === "system");
424
+ const nonSystemMessages = messages.filter((msg) => msg.role !== "system");
425
+ if (systemMessages.length === 0) {
426
+ return { systemPrompt: void 0, messages };
427
+ }
428
+ const systemPrompt = systemMessages.map((msg) => msg.content).filter((content) => typeof content === "string").join("\n\n");
429
+ return {
430
+ systemPrompt: systemPrompt || void 0,
431
+ messages: nonSystemMessages
432
+ };
433
+ }
434
+ function prependSystemPromptToMessages(messages, systemPrompt) {
435
+ if (!systemPrompt.trim()) {
436
+ return messages;
437
+ }
438
+ const systemMessageIndex = messages.findIndex((msg) => msg.role === "system");
439
+ if (systemMessageIndex !== -1) {
440
+ const newMessages = [...messages];
441
+ const existingSystemMessage = messages[systemMessageIndex];
442
+ const existingContent = typeof existingSystemMessage.content === "string" ? existingSystemMessage.content : "";
443
+ newMessages[systemMessageIndex] = {
444
+ ...existingSystemMessage,
445
+ content: systemPrompt + (existingContent ? `
446
+
447
+ ${existingContent}` : "")
448
+ };
449
+ return newMessages;
450
+ }
451
+ return [
452
+ {
453
+ role: "system",
454
+ content: systemPrompt
455
+ },
456
+ ...messages
457
+ ];
458
+ }
459
+
460
+ // src/streaming/tool-call-detector.ts
461
+ var ToolCallFenceDetector = class {
462
+ constructor() {
463
+ this.FENCE_STARTS = ["```tool_call"];
464
+ this.FENCE_END = "```";
465
+ this.buffer = "";
466
+ this.inFence = false;
467
+ this.fenceStartBuffer = "";
468
+ }
469
+ addChunk(chunk) {
470
+ this.buffer += chunk;
471
+ }
472
+ getBuffer() {
473
+ return this.buffer;
474
+ }
475
+ clearBuffer() {
476
+ this.buffer = "";
477
+ }
478
+ /**
479
+ * Detects if there's a complete fence in the buffer
480
+ *
481
+ * 1. Searches for fence start markers
482
+ * 2. If found, looks for closing fence
483
+ * 3. Computes overlap for partial fences
484
+ * 4. Returns safe text that can be emitted
485
+ *
486
+ * @returns Detection result with fence info and safe text
487
+ */
488
+ detectFence() {
489
+ const { index: startIdx, prefix: matchedPrefix } = this.findFenceStart(
490
+ this.buffer
491
+ );
492
+ if (startIdx === -1) {
493
+ const overlap = this.computeOverlapLength(this.buffer, this.FENCE_STARTS);
494
+ const safeTextLength = this.buffer.length - overlap;
495
+ const prefixText2 = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
496
+ const remaining = overlap > 0 ? this.buffer.slice(-overlap) : "";
497
+ this.buffer = remaining;
498
+ return {
499
+ fence: null,
500
+ prefixText: prefixText2,
501
+ remainingText: "",
502
+ overlapLength: overlap
503
+ };
504
+ }
505
+ const prefixText = this.buffer.slice(0, startIdx);
506
+ this.buffer = this.buffer.slice(startIdx);
507
+ const prefixLength = matchedPrefix?.length ?? 0;
508
+ const closingIdx = this.buffer.indexOf(this.FENCE_END, prefixLength);
509
+ if (closingIdx === -1) {
510
+ return {
511
+ fence: null,
512
+ prefixText,
513
+ remainingText: "",
514
+ overlapLength: 0
515
+ };
516
+ }
517
+ const endPos = closingIdx + this.FENCE_END.length;
518
+ const fence = this.buffer.slice(0, endPos);
519
+ const remainingText = this.buffer.slice(endPos);
520
+ this.buffer = "";
521
+ return {
522
+ fence,
523
+ prefixText,
524
+ remainingText,
525
+ overlapLength: 0
526
+ };
527
+ }
528
+ /**
529
+ * Finds the first occurrence of any fence start marker
530
+ *
531
+ * @param text - Text to search in
532
+ * @returns Index of first fence start and which prefix matched
533
+ * @private
534
+ */
535
+ findFenceStart(text) {
536
+ let bestIndex = -1;
537
+ let matchedPrefix = null;
538
+ for (const prefix of this.FENCE_STARTS) {
539
+ const idx = text.indexOf(prefix);
540
+ if (idx !== -1 && (bestIndex === -1 || idx < bestIndex)) {
541
+ bestIndex = idx;
542
+ matchedPrefix = prefix;
543
+ }
544
+ }
545
+ return { index: bestIndex, prefix: matchedPrefix };
546
+ }
547
+ computeOverlapLength(text, prefixes) {
548
+ let overlap = 0;
549
+ for (const prefix of prefixes) {
550
+ const maxLength = Math.min(text.length, prefix.length - 1);
551
+ for (let size = maxLength; size > 0; size -= 1) {
552
+ if (prefix.startsWith(text.slice(-size))) {
553
+ overlap = Math.max(overlap, size);
554
+ break;
555
+ }
556
+ }
557
+ }
558
+ return overlap;
559
+ }
560
+ hasContent() {
561
+ return this.buffer.length > 0;
562
+ }
563
+ getBufferSize() {
564
+ return this.buffer.length;
565
+ }
566
+ /**
567
+ * Detect and stream fence content in real-time for true incremental streaming
568
+ *
569
+ * This method is designed for streaming tool calls as they arrive:
570
+ * 1. Detects when a fence starts and transitions to "inFence" state
571
+ * 2. While inFence, emits safe content that won't conflict with fence end marker
572
+ * 3. When fence ends, returns the complete fence for parsing
573
+ *
574
+ * @returns Streaming result with current state and safe content to emit
575
+ */
576
+ detectStreamingFence() {
577
+ if (!this.inFence) {
578
+ const { index: startIdx, prefix: matchedPrefix } = this.findFenceStart(
579
+ this.buffer
580
+ );
581
+ if (startIdx === -1) {
582
+ const overlap = this.computeOverlapLength(
583
+ this.buffer,
584
+ this.FENCE_STARTS
585
+ );
586
+ const safeTextLength = this.buffer.length - overlap;
587
+ const safeContent = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
588
+ this.buffer = this.buffer.slice(safeTextLength);
589
+ return {
590
+ inFence: false,
591
+ safeContent,
592
+ completeFence: null,
593
+ textAfterFence: ""
594
+ };
595
+ }
596
+ const prefixText = this.buffer.slice(0, startIdx);
597
+ const fenceStartLength = matchedPrefix?.length ?? 0;
598
+ this.buffer = this.buffer.slice(startIdx + fenceStartLength);
599
+ if (this.buffer.startsWith("\n")) {
600
+ this.buffer = this.buffer.slice(1);
601
+ }
602
+ this.inFence = true;
603
+ this.fenceStartBuffer = "";
604
+ return {
605
+ inFence: true,
606
+ safeContent: prefixText,
607
+ completeFence: null,
608
+ textAfterFence: ""
609
+ };
610
+ }
611
+ const closingIdx = this.buffer.indexOf(this.FENCE_END);
612
+ if (closingIdx === -1) {
613
+ const overlap = this.computeOverlapLength(this.buffer, [this.FENCE_END]);
614
+ const safeContentLength = this.buffer.length - overlap;
615
+ if (safeContentLength > 0) {
616
+ const safeContent = this.buffer.slice(0, safeContentLength);
617
+ this.fenceStartBuffer += safeContent;
618
+ this.buffer = this.buffer.slice(safeContentLength);
619
+ return {
620
+ inFence: true,
621
+ safeContent,
622
+ completeFence: null,
623
+ textAfterFence: ""
624
+ };
625
+ }
626
+ return {
627
+ inFence: true,
628
+ safeContent: "",
629
+ completeFence: null,
630
+ textAfterFence: ""
631
+ };
632
+ }
633
+ const fenceContent = this.buffer.slice(0, closingIdx);
634
+ this.fenceStartBuffer += fenceContent;
635
+ const completeFence = `${this.FENCE_STARTS[0]}
636
+ ${this.fenceStartBuffer}
637
+ ${this.FENCE_END}`;
638
+ const textAfterFence = this.buffer.slice(
639
+ closingIdx + this.FENCE_END.length
640
+ );
641
+ this.inFence = false;
642
+ this.fenceStartBuffer = "";
643
+ this.buffer = textAfterFence;
644
+ return {
645
+ inFence: false,
646
+ safeContent: fenceContent,
647
+ completeFence,
648
+ textAfterFence
649
+ };
650
+ }
651
+ isInFence() {
652
+ return this.inFence;
653
+ }
654
+ resetStreamingState() {
655
+ this.inFence = false;
656
+ this.fenceStartBuffer = "";
657
+ }
658
+ };
659
+
660
+ // src/web-llm-language-model.ts
661
+ function doesBrowserSupportWebLLM() {
662
+ return globalThis?.navigator?.gpu !== void 0;
663
+ }
664
+ function extractToolName(content) {
665
+ const jsonMatch = content.match(/\{\s*"name"\s*:\s*"([^"]+)"/);
666
+ if (jsonMatch) {
667
+ return jsonMatch[1];
668
+ }
669
+ return null;
670
+ }
671
+ function extractArgumentsContent(content) {
672
+ const match = content.match(/"arguments"\s*:\s*/);
673
+ if (!match || match.index === void 0) {
674
+ return "";
675
+ }
676
+ const startIndex = match.index + match[0].length;
677
+ let result = "";
678
+ let depth = 0;
679
+ let inString = false;
680
+ let escaped = false;
681
+ let started = false;
682
+ for (let i = startIndex; i < content.length; i++) {
683
+ const char = content[i];
684
+ result += char;
685
+ if (!started) {
686
+ if (!/\s/.test(char)) {
687
+ started = true;
688
+ if (char === "{" || char === "[") {
689
+ depth = 1;
690
+ }
691
+ }
692
+ continue;
693
+ }
694
+ if (escaped) {
695
+ escaped = false;
696
+ continue;
697
+ }
698
+ if (char === "\\") {
699
+ escaped = true;
700
+ continue;
701
+ }
702
+ if (char === '"') {
703
+ inString = !inString;
704
+ continue;
705
+ }
706
+ if (!inString) {
707
+ if (char === "{" || char === "[") {
708
+ depth += 1;
709
+ } else if (char === "}" || char === "]") {
710
+ if (depth > 0) {
711
+ depth -= 1;
712
+ if (depth === 0) {
713
+ break;
714
+ }
715
+ }
716
+ }
717
+ }
718
+ }
719
+ return result;
720
+ }
721
+ var WebLLMLanguageModel = class {
722
+ constructor(modelId, options = {}) {
723
+ this.specificationVersion = "v2";
724
+ this.provider = "web-llm";
725
+ this.isInitialized = false;
726
+ this.supportedUrls = {
727
+ // WebLLM doesn't support URLs natively
728
+ };
729
+ this.modelId = modelId;
730
+ this.config = {
731
+ provider: this.provider,
732
+ modelId,
733
+ options
734
+ };
735
+ }
736
+ /**
737
+ * Check if the model is initialized and ready to use
738
+ * @returns true if the model is initialized, false otherwise
739
+ */
740
+ get isModelInitialized() {
741
+ return this.isInitialized;
742
+ }
743
+ async getEngine(options, onInitProgress) {
744
+ const availability = await this.availability();
745
+ if (availability === "unavailable") {
746
+ throw new LoadSettingError({
747
+ message: "WebLLM is not available. This library requires a browser with WebGPU support."
748
+ });
749
+ }
750
+ if (this.engine && this.isInitialized) return this.engine;
751
+ if (this.initializationPromise) {
752
+ await this.initializationPromise;
753
+ if (this.engine) return this.engine;
754
+ }
755
+ this.initializationPromise = this._initializeEngine(
756
+ options,
757
+ onInitProgress
758
+ );
759
+ await this.initializationPromise;
760
+ if (!this.engine) {
761
+ throw new LoadSettingError({
762
+ message: "Engine initialization failed"
763
+ });
764
+ }
765
+ return this.engine;
766
+ }
767
+ async _initializeEngine(options, onInitProgress) {
768
+ try {
769
+ const engineConfig = {
770
+ ...this.config.options.engineConfig,
771
+ ...options,
772
+ initProgressCallback: onInitProgress || this.config.options.initProgressCallback
773
+ };
774
+ if (this.config.options.worker) {
775
+ this.engine = await CreateWebWorkerMLCEngine(
776
+ this.config.options.worker,
777
+ this.modelId,
778
+ engineConfig
779
+ );
780
+ } else {
781
+ this.engine = new MLCEngine(engineConfig);
782
+ await this.engine.reload(this.modelId);
783
+ }
784
+ this.isInitialized = true;
785
+ } catch (error) {
786
+ this.engine = void 0;
787
+ this.isInitialized = false;
788
+ this.initializationPromise = void 0;
789
+ throw new LoadSettingError({
790
+ message: `Failed to initialize WebLLM engine: ${error instanceof Error ? error.message : "Unknown error"}`
791
+ });
792
+ }
793
+ }
794
+ getArgs({
795
+ prompt,
796
+ maxOutputTokens,
797
+ temperature,
798
+ topP,
799
+ topK,
800
+ frequencyPenalty,
801
+ presencePenalty,
802
+ stopSequences,
803
+ responseFormat,
804
+ seed,
805
+ tools,
806
+ toolChoice
807
+ }) {
808
+ const warnings = [];
809
+ const functionTools = (tools ?? []).filter(isFunctionTool).map((tool) => ({
810
+ name: tool.name,
811
+ description: tool.description,
812
+ parameters: tool.inputSchema
813
+ }));
814
+ const unsupportedTools = (tools ?? []).filter(
815
+ (tool) => !isFunctionTool(tool)
816
+ );
817
+ for (const tool of unsupportedTools) {
818
+ warnings.push(
819
+ createUnsupportedToolWarning(
820
+ tool,
821
+ "Only function tools are supported by WebLLM"
822
+ )
823
+ );
824
+ }
825
+ if (topK != null) {
826
+ warnings.push(
827
+ createUnsupportedSettingWarning(
828
+ "topK",
829
+ "topK is not supported by WebLLM"
830
+ )
831
+ );
832
+ }
833
+ if (stopSequences != null) {
834
+ warnings.push(
835
+ createUnsupportedSettingWarning(
836
+ "stopSequences",
837
+ "Stop sequences may not be fully implemented"
838
+ )
839
+ );
840
+ }
841
+ if (presencePenalty != null) {
842
+ warnings.push(
843
+ createUnsupportedSettingWarning(
844
+ "presencePenalty",
845
+ "Presence penalty is not fully implemented"
846
+ )
847
+ );
848
+ }
849
+ if (frequencyPenalty != null) {
850
+ warnings.push(
851
+ createUnsupportedSettingWarning(
852
+ "frequencyPenalty",
853
+ "Frequency penalty is not fully implemented"
854
+ )
855
+ );
856
+ }
857
+ if (toolChoice != null) {
858
+ warnings.push(
859
+ createUnsupportedSettingWarning(
860
+ "toolChoice",
861
+ "toolChoice is not supported by WebLLM"
862
+ )
863
+ );
864
+ }
865
+ const messages = convertToWebLLMMessages(prompt);
866
+ const requestOptions = {
867
+ messages,
868
+ temperature,
869
+ max_tokens: maxOutputTokens,
870
+ top_p: topP,
871
+ seed
872
+ };
873
+ if (responseFormat?.type === "json") {
874
+ requestOptions.response_format = { type: "json_object" };
875
+ }
876
+ return {
877
+ messages,
878
+ warnings,
879
+ requestOptions,
880
+ functionTools
881
+ };
882
+ }
883
+ /**
884
+ * Generates a complete text response using WebLLM
885
+ * @param options
886
+ * @returns Promise resolving to the generated content with finish reason, usage stats, and any warnings
887
+ * @throws {LoadSettingError} When WebLLM is not available or model needs to be downloaded
888
+ * @throws {UnsupportedFunctionalityError} When unsupported features like file input are used
889
+ */
890
+ async doGenerate(options) {
891
+ const converted = this.getArgs(options);
892
+ const { messages, warnings, requestOptions, functionTools } = converted;
893
+ const {
894
+ systemPrompt: originalSystemPrompt,
895
+ messages: messagesWithoutSystem
896
+ } = extractSystemPrompt(messages);
897
+ const systemPrompt = buildJsonToolSystemPrompt(
898
+ originalSystemPrompt,
899
+ functionTools,
900
+ {
901
+ allowParallelToolCalls: false
902
+ }
903
+ );
904
+ const promptMessages = prependSystemPromptToMessages(
905
+ messagesWithoutSystem,
906
+ systemPrompt
907
+ );
908
+ const engine = await this.getEngine();
909
+ const abortHandler = async () => {
910
+ await engine.interruptGenerate();
911
+ };
912
+ if (options.abortSignal) {
913
+ options.abortSignal.addEventListener("abort", abortHandler);
914
+ }
915
+ try {
916
+ const response = await engine.chat.completions.create({
917
+ ...requestOptions,
918
+ messages: promptMessages,
919
+ stream: false,
920
+ ...options.abortSignal && !this.config.options.worker && { signal: options.abortSignal }
921
+ });
922
+ const choice = response.choices[0];
923
+ if (!choice) {
924
+ throw new Error("No response choice returned from WebLLM");
925
+ }
926
+ const rawResponse = choice.message.content || "";
927
+ const { toolCalls, textContent } = parseJsonFunctionCalls(rawResponse);
928
+ if (toolCalls.length > 0) {
929
+ const toolCallsToEmit = toolCalls.slice(0, 1);
930
+ const parts = [];
931
+ if (textContent) {
932
+ parts.push({
933
+ type: "text",
934
+ text: textContent
935
+ });
936
+ }
937
+ for (const call of toolCallsToEmit) {
938
+ parts.push({
939
+ type: "tool-call",
940
+ toolCallId: call.toolCallId,
941
+ toolName: call.toolName,
942
+ input: JSON.stringify(call.args ?? {})
943
+ });
944
+ }
945
+ return {
946
+ content: parts,
947
+ finishReason: "tool-calls",
948
+ usage: {
949
+ inputTokens: response.usage?.prompt_tokens,
950
+ outputTokens: response.usage?.completion_tokens,
951
+ totalTokens: response.usage?.total_tokens
952
+ },
953
+ request: { body: { messages: promptMessages, ...requestOptions } },
954
+ warnings
955
+ };
956
+ }
957
+ const content = [
958
+ {
959
+ type: "text",
960
+ text: textContent || rawResponse
961
+ }
962
+ ];
963
+ let finishReason = "stop";
964
+ if (choice.finish_reason === "abort") {
965
+ finishReason = "other";
966
+ }
967
+ return {
968
+ content,
969
+ finishReason,
970
+ usage: {
971
+ inputTokens: response.usage?.prompt_tokens,
972
+ outputTokens: response.usage?.completion_tokens,
973
+ totalTokens: response.usage?.total_tokens
974
+ },
975
+ request: { body: { messages: promptMessages, ...requestOptions } },
976
+ warnings
977
+ };
978
+ } catch (error) {
979
+ throw new Error(
980
+ `WebLLM generation failed: ${error instanceof Error ? error.message : "Unknown error"}`
981
+ );
982
+ } finally {
983
+ if (options.abortSignal) {
984
+ options.abortSignal.removeEventListener("abort", abortHandler);
985
+ }
986
+ }
987
+ }
988
+ /**
989
+ * Check the availability of the WebLLM model
990
+ * @returns Promise resolving to "unavailable", "available", or "available-after-download"
991
+ */
992
+ async availability() {
993
+ if (!doesBrowserSupportWebLLM()) {
994
+ return "unavailable";
995
+ }
996
+ if (this.isInitialized) {
997
+ return "available";
998
+ }
999
+ return "downloadable";
1000
+ }
1001
+ /**
1002
+ * Creates an engine session with download progress monitoring.
1003
+ *
1004
+ * @example
1005
+ * ```typescript
1006
+ * const engine = await model.createSessionWithProgress(
1007
+ * (progress) => {
1008
+ * console.log(`Download progress: ${Math.round(progress.loaded * 100)}%`);
1009
+ * }
1010
+ * );
1011
+ * ```
1012
+ *
1013
+ * @param onInitProgress Optional callback receiving progress reports during model download
1014
+ * @returns Promise resolving to a configured WebLLM engine
1015
+ * @throws {LoadSettingError} When WebLLM is not available or model is unavailable
1016
+ */
1017
+ async createSessionWithProgress(onInitProgress) {
1018
+ return this.getEngine(void 0, onInitProgress);
1019
+ }
1020
+ /**
1021
+ * Generates a streaming text response using WebLLM
1022
+ * @param options
1023
+ * @returns Promise resolving to a readable stream of text chunks and request metadata
1024
+ * @throws {LoadSettingError} When WebLLM is not available or model needs to be downloaded
1025
+ * @throws {UnsupportedFunctionalityError} When unsupported features like file input are used
1026
+ */
1027
+ async doStream(options) {
1028
+ const converted = this.getArgs(options);
1029
+ const { messages, warnings, requestOptions, functionTools } = converted;
1030
+ const {
1031
+ systemPrompt: originalSystemPrompt,
1032
+ messages: messagesWithoutSystem
1033
+ } = extractSystemPrompt(messages);
1034
+ const systemPrompt = buildJsonToolSystemPrompt(
1035
+ originalSystemPrompt,
1036
+ functionTools,
1037
+ {
1038
+ allowParallelToolCalls: false
1039
+ }
1040
+ );
1041
+ const promptMessages = prependSystemPromptToMessages(
1042
+ messagesWithoutSystem,
1043
+ systemPrompt
1044
+ );
1045
+ const engine = await this.getEngine();
1046
+ const useWorker = this.config.options.worker != null;
1047
+ const abortHandler = async () => {
1048
+ await engine.interruptGenerate();
1049
+ };
1050
+ if (options.abortSignal) {
1051
+ options.abortSignal.addEventListener("abort", abortHandler);
1052
+ }
1053
+ const textId = "text-0";
1054
+ const stream = new ReadableStream({
1055
+ async start(controller) {
1056
+ controller.enqueue({
1057
+ type: "stream-start",
1058
+ warnings
1059
+ });
1060
+ let textStarted = false;
1061
+ let finished = false;
1062
+ const ensureTextStart = () => {
1063
+ if (!textStarted) {
1064
+ controller.enqueue({
1065
+ type: "text-start",
1066
+ id: textId
1067
+ });
1068
+ textStarted = true;
1069
+ }
1070
+ };
1071
+ const emitTextDelta = (delta) => {
1072
+ if (!delta) return;
1073
+ ensureTextStart();
1074
+ controller.enqueue({
1075
+ type: "text-delta",
1076
+ id: textId,
1077
+ delta
1078
+ });
1079
+ };
1080
+ const emitTextEndIfNeeded = () => {
1081
+ if (!textStarted) return;
1082
+ controller.enqueue({
1083
+ type: "text-end",
1084
+ id: textId
1085
+ });
1086
+ textStarted = false;
1087
+ };
1088
+ const finishStream = (finishReason, usage) => {
1089
+ if (finished) return;
1090
+ finished = true;
1091
+ emitTextEndIfNeeded();
1092
+ controller.enqueue({
1093
+ type: "finish",
1094
+ finishReason,
1095
+ usage: {
1096
+ inputTokens: usage?.prompt_tokens,
1097
+ outputTokens: usage?.completion_tokens,
1098
+ totalTokens: usage?.total_tokens
1099
+ }
1100
+ });
1101
+ controller.close();
1102
+ };
1103
+ try {
1104
+ const streamingRequest = {
1105
+ ...requestOptions,
1106
+ messages: promptMessages,
1107
+ stream: true,
1108
+ stream_options: { include_usage: true },
1109
+ ...options.abortSignal && !useWorker && { signal: options.abortSignal }
1110
+ };
1111
+ const response = await engine.chat.completions.create(streamingRequest);
1112
+ const fenceDetector = new ToolCallFenceDetector();
1113
+ let accumulatedText = "";
1114
+ let currentToolCallId = null;
1115
+ let toolInputStartEmitted = false;
1116
+ let accumulatedFenceContent = "";
1117
+ let streamedArgumentsLength = 0;
1118
+ let insideFence = false;
1119
+ for await (const chunk of response) {
1120
+ const choice = chunk.choices[0];
1121
+ if (!choice) continue;
1122
+ if (choice.delta.content) {
1123
+ const delta = choice.delta.content;
1124
+ accumulatedText += delta;
1125
+ fenceDetector.addChunk(delta);
1126
+ while (fenceDetector.hasContent()) {
1127
+ const wasInsideFence = insideFence;
1128
+ const result = fenceDetector.detectStreamingFence();
1129
+ insideFence = result.inFence;
1130
+ let madeProgress = false;
1131
+ if (!wasInsideFence && result.inFence) {
1132
+ if (result.safeContent) {
1133
+ emitTextDelta(result.safeContent);
1134
+ madeProgress = true;
1135
+ }
1136
+ currentToolCallId = `call_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
1137
+ toolInputStartEmitted = false;
1138
+ accumulatedFenceContent = "";
1139
+ streamedArgumentsLength = 0;
1140
+ insideFence = true;
1141
+ continue;
1142
+ }
1143
+ if (result.completeFence) {
1144
+ madeProgress = true;
1145
+ if (result.safeContent) {
1146
+ accumulatedFenceContent += result.safeContent;
1147
+ }
1148
+ if (toolInputStartEmitted && currentToolCallId) {
1149
+ const argsContent = extractArgumentsContent(
1150
+ accumulatedFenceContent
1151
+ );
1152
+ if (argsContent.length > streamedArgumentsLength) {
1153
+ const delta2 = argsContent.slice(streamedArgumentsLength);
1154
+ streamedArgumentsLength = argsContent.length;
1155
+ if (delta2.length > 0) {
1156
+ controller.enqueue({
1157
+ type: "tool-input-delta",
1158
+ id: currentToolCallId,
1159
+ delta: delta2
1160
+ });
1161
+ }
1162
+ }
1163
+ }
1164
+ const parsed = parseJsonFunctionCalls(result.completeFence);
1165
+ const parsedToolCalls = parsed.toolCalls;
1166
+ const selectedToolCalls = parsedToolCalls.slice(0, 1);
1167
+ if (selectedToolCalls.length === 0) {
1168
+ emitTextDelta(result.completeFence);
1169
+ if (result.textAfterFence) {
1170
+ emitTextDelta(result.textAfterFence);
1171
+ }
1172
+ currentToolCallId = null;
1173
+ toolInputStartEmitted = false;
1174
+ accumulatedFenceContent = "";
1175
+ streamedArgumentsLength = 0;
1176
+ insideFence = false;
1177
+ continue;
1178
+ }
1179
+ if (selectedToolCalls.length > 0 && currentToolCallId) {
1180
+ selectedToolCalls[0].toolCallId = currentToolCallId;
1181
+ }
1182
+ for (const [index, call] of selectedToolCalls.entries()) {
1183
+ const toolCallId = index === 0 && currentToolCallId ? currentToolCallId : call.toolCallId;
1184
+ const toolName = call.toolName;
1185
+ const argsJson = JSON.stringify(call.args ?? {});
1186
+ if (toolCallId === currentToolCallId) {
1187
+ if (!toolInputStartEmitted) {
1188
+ controller.enqueue({
1189
+ type: "tool-input-start",
1190
+ id: toolCallId,
1191
+ toolName
1192
+ });
1193
+ toolInputStartEmitted = true;
1194
+ }
1195
+ const argsContent = extractArgumentsContent(
1196
+ accumulatedFenceContent
1197
+ );
1198
+ if (argsContent.length > streamedArgumentsLength) {
1199
+ const delta2 = argsContent.slice(
1200
+ streamedArgumentsLength
1201
+ );
1202
+ streamedArgumentsLength = argsContent.length;
1203
+ if (delta2.length > 0) {
1204
+ controller.enqueue({
1205
+ type: "tool-input-delta",
1206
+ id: toolCallId,
1207
+ delta: delta2
1208
+ });
1209
+ }
1210
+ }
1211
+ } else {
1212
+ controller.enqueue({
1213
+ type: "tool-input-start",
1214
+ id: toolCallId,
1215
+ toolName
1216
+ });
1217
+ if (argsJson.length > 0) {
1218
+ controller.enqueue({
1219
+ type: "tool-input-delta",
1220
+ id: toolCallId,
1221
+ delta: argsJson
1222
+ });
1223
+ }
1224
+ }
1225
+ controller.enqueue({
1226
+ type: "tool-input-end",
1227
+ id: toolCallId
1228
+ });
1229
+ controller.enqueue({
1230
+ type: "tool-call",
1231
+ toolCallId,
1232
+ toolName,
1233
+ input: argsJson,
1234
+ providerExecuted: false
1235
+ });
1236
+ }
1237
+ if (result.textAfterFence) {
1238
+ emitTextDelta(result.textAfterFence);
1239
+ }
1240
+ madeProgress = true;
1241
+ currentToolCallId = null;
1242
+ toolInputStartEmitted = false;
1243
+ accumulatedFenceContent = "";
1244
+ streamedArgumentsLength = 0;
1245
+ insideFence = false;
1246
+ continue;
1247
+ }
1248
+ if (insideFence) {
1249
+ if (result.safeContent) {
1250
+ accumulatedFenceContent += result.safeContent;
1251
+ madeProgress = true;
1252
+ const toolName = extractToolName(accumulatedFenceContent);
1253
+ if (toolName && !toolInputStartEmitted && currentToolCallId) {
1254
+ controller.enqueue({
1255
+ type: "tool-input-start",
1256
+ id: currentToolCallId,
1257
+ toolName
1258
+ });
1259
+ toolInputStartEmitted = true;
1260
+ }
1261
+ if (toolInputStartEmitted && currentToolCallId) {
1262
+ const argsContent = extractArgumentsContent(
1263
+ accumulatedFenceContent
1264
+ );
1265
+ if (argsContent.length > streamedArgumentsLength) {
1266
+ const delta2 = argsContent.slice(
1267
+ streamedArgumentsLength
1268
+ );
1269
+ streamedArgumentsLength = argsContent.length;
1270
+ if (delta2.length > 0) {
1271
+ controller.enqueue({
1272
+ type: "tool-input-delta",
1273
+ id: currentToolCallId,
1274
+ delta: delta2
1275
+ });
1276
+ }
1277
+ }
1278
+ }
1279
+ }
1280
+ continue;
1281
+ }
1282
+ if (!insideFence && result.safeContent) {
1283
+ emitTextDelta(result.safeContent);
1284
+ madeProgress = true;
1285
+ }
1286
+ if (!madeProgress) {
1287
+ break;
1288
+ }
1289
+ }
1290
+ }
1291
+ if (choice.finish_reason) {
1292
+ if (fenceDetector.hasContent()) {
1293
+ emitTextDelta(fenceDetector.getBuffer());
1294
+ fenceDetector.clearBuffer();
1295
+ }
1296
+ let finishReason = "stop";
1297
+ if (choice.finish_reason === "abort") {
1298
+ finishReason = "other";
1299
+ } else {
1300
+ const { toolCalls } = parseJsonFunctionCalls(accumulatedText);
1301
+ if (toolCalls.length > 0) {
1302
+ finishReason = "tool-calls";
1303
+ }
1304
+ }
1305
+ finishStream(finishReason, chunk.usage);
1306
+ }
1307
+ }
1308
+ if (!finished) {
1309
+ finishStream("stop");
1310
+ }
1311
+ } catch (error) {
1312
+ controller.error(error);
1313
+ } finally {
1314
+ if (options.abortSignal) {
1315
+ options.abortSignal.removeEventListener("abort", abortHandler);
1316
+ }
1317
+ if (!finished) {
1318
+ controller.close();
1319
+ }
1320
+ }
1321
+ }
1322
+ });
1323
+ return {
1324
+ stream,
1325
+ request: { body: { messages: promptMessages, ...requestOptions } }
1326
+ };
1327
+ }
1328
+ };
1329
+
1330
+ // src/index.ts
1331
+ import { WebWorkerMLCEngineHandler } from "@mlc-ai/web-llm";
1332
+
1333
+ // src/web-llm-provider.ts
1334
+ function webLLM(modelId, settings) {
1335
+ return new WebLLMLanguageModel(modelId, settings);
1336
+ }
1337
+ export {
1338
+ WebLLMLanguageModel,
1339
+ WebWorkerMLCEngineHandler,
1340
+ doesBrowserSupportWebLLM,
1341
+ webLLM
1342
+ };
1343
+ //# sourceMappingURL=index.mjs.map