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