@browser-ai/web-llm 2.0.2 → 2.0.4

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 CHANGED
@@ -317,7 +317,28 @@ var UnsupportedFunctionalityError = class extends (_b14 = AISDKError, _a14 = sym
317
317
  }
318
318
  };
319
319
 
320
- // src/tool-calling/build-json-system-prompt.ts
320
+ // ../shared/src/utils/tool-utils.ts
321
+ function isFunctionTool(tool) {
322
+ return tool.type === "function";
323
+ }
324
+
325
+ // ../shared/src/utils/warnings.ts
326
+ function createUnsupportedSettingWarning(feature, details) {
327
+ return {
328
+ type: "unsupported",
329
+ feature,
330
+ details
331
+ };
332
+ }
333
+ function createUnsupportedToolWarning(tool, details) {
334
+ return {
335
+ type: "unsupported",
336
+ feature: `tool:${tool.name}`,
337
+ details
338
+ };
339
+ }
340
+
341
+ // ../shared/src/tool-calling/build-json-system-prompt.ts
321
342
  function buildJsonToolSystemPrompt(originalSystemPrompt, tools, options) {
322
343
  if (!tools || tools.length === 0) {
323
344
  return originalSystemPrompt || "";
@@ -371,34 +392,112 @@ function getParameters(tool) {
371
392
  return tool.inputSchema;
372
393
  }
373
394
 
374
- // src/tool-calling/parse-json-function-calls.ts
375
- var JSON_TOOL_CALL_FENCE_REGEX = /```tool[_-]?call\s*([\s\S]*?)```/gi;
395
+ // ../shared/src/tool-calling/format-tool-results.ts
396
+ function buildResultPayload(result) {
397
+ const payload = {
398
+ name: result.toolName,
399
+ result: result.result ?? null,
400
+ error: Boolean(result.isError)
401
+ };
402
+ if (result.toolCallId) {
403
+ payload.id = result.toolCallId;
404
+ }
405
+ return payload;
406
+ }
407
+ function formatToolResults(results) {
408
+ if (!results || results.length === 0) {
409
+ return "";
410
+ }
411
+ const payloads = results.map(
412
+ (result) => JSON.stringify(buildResultPayload(result))
413
+ );
414
+ return `\`\`\`tool_result
415
+ ${payloads.join("\n")}
416
+ \`\`\``;
417
+ }
418
+
419
+ // ../shared/src/tool-calling/parse-json-function-calls.ts
420
+ var DEFAULT_OPTIONS = {
421
+ supportXmlTags: true,
422
+ supportPythonStyle: true,
423
+ supportParametersField: true
424
+ };
376
425
  function generateToolCallId() {
377
426
  return `call_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
378
427
  }
379
- function parseJsonFunctionCalls(response) {
380
- const matches = Array.from(response.matchAll(JSON_TOOL_CALL_FENCE_REGEX));
381
- JSON_TOOL_CALL_FENCE_REGEX.lastIndex = 0;
428
+ function buildRegex(options) {
429
+ const patterns = [];
430
+ patterns.push("```tool[_-]?call\\s*([\\s\\S]*?)```");
431
+ if (options.supportXmlTags) {
432
+ patterns.push("<tool_call>\\s*([\\s\\S]*?)\\s*</tool_call>");
433
+ }
434
+ if (options.supportPythonStyle) {
435
+ patterns.push("\\[(\\w+)\\(([^)]*)\\)\\]");
436
+ }
437
+ return new RegExp(patterns.join("|"), "gi");
438
+ }
439
+ function parseJsonFunctionCalls(response, options = DEFAULT_OPTIONS) {
440
+ const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
441
+ const regex = buildRegex(mergedOptions);
442
+ const matches = Array.from(response.matchAll(regex));
443
+ regex.lastIndex = 0;
382
444
  if (matches.length === 0) {
383
445
  return { toolCalls: [], textContent: response };
384
446
  }
385
447
  const toolCalls = [];
386
448
  let textContent = response;
387
449
  for (const match of matches) {
388
- const [fullFence, innerContent] = match;
389
- textContent = textContent.replace(fullFence, "");
450
+ const fullMatch = match[0];
451
+ textContent = textContent.replace(fullMatch, "");
390
452
  try {
453
+ if (mergedOptions.supportPythonStyle && match[0].startsWith("[")) {
454
+ const pythonMatch = /\[(\w+)\(([^)]*)\)\]/.exec(match[0]);
455
+ if (pythonMatch) {
456
+ const [, funcName, pythonArgs] = pythonMatch;
457
+ const args = {};
458
+ if (pythonArgs && pythonArgs.trim()) {
459
+ const argPairs = pythonArgs.split(",").map((s) => s.trim());
460
+ for (const pair of argPairs) {
461
+ const equalIndex = pair.indexOf("=");
462
+ if (equalIndex > 0) {
463
+ const key = pair.substring(0, equalIndex).trim();
464
+ let value = pair.substring(equalIndex + 1).trim();
465
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
466
+ value = value.substring(1, value.length - 1);
467
+ }
468
+ args[key] = value;
469
+ }
470
+ }
471
+ }
472
+ toolCalls.push({
473
+ type: "tool-call",
474
+ toolCallId: generateToolCallId(),
475
+ toolName: funcName,
476
+ args
477
+ });
478
+ continue;
479
+ }
480
+ }
481
+ const innerContent = match[1] || match[2] || "";
391
482
  const trimmed = innerContent.trim();
483
+ if (!trimmed) continue;
392
484
  try {
393
485
  const parsed = JSON.parse(trimmed);
394
486
  const callsArray = Array.isArray(parsed) ? parsed : [parsed];
395
487
  for (const call of callsArray) {
396
488
  if (!call.name) continue;
489
+ let args = call.arguments || (mergedOptions.supportParametersField ? call.parameters : null) || {};
490
+ if (typeof args === "string") {
491
+ try {
492
+ args = JSON.parse(args);
493
+ } catch {
494
+ }
495
+ }
397
496
  toolCalls.push({
398
497
  type: "tool-call",
399
498
  toolCallId: call.id || generateToolCallId(),
400
499
  toolName: call.name,
401
- args: call.arguments || {}
500
+ args
402
501
  });
403
502
  }
404
503
  } catch {
@@ -407,11 +506,18 @@ function parseJsonFunctionCalls(response) {
407
506
  try {
408
507
  const call = JSON.parse(line.trim());
409
508
  if (!call.name) continue;
509
+ let args = call.arguments || (mergedOptions.supportParametersField ? call.parameters : null) || {};
510
+ if (typeof args === "string") {
511
+ try {
512
+ args = JSON.parse(args);
513
+ } catch {
514
+ }
515
+ }
410
516
  toolCalls.push({
411
517
  type: "tool-call",
412
518
  toolCallId: call.id || generateToolCallId(),
413
519
  toolName: call.name,
414
- args: call.arguments || {}
520
+ args
415
521
  });
416
522
  } catch {
417
523
  continue;
@@ -427,24 +533,245 @@ function parseJsonFunctionCalls(response) {
427
533
  return { toolCalls, textContent: textContent.trim() };
428
534
  }
429
535
 
430
- // src/tool-calling/format-tool-results.ts
431
- function formatToolResults(results) {
432
- if (results.length === 0) {
433
- return "";
536
+ // ../shared/src/streaming/tool-call-detector.ts
537
+ var DEFAULT_FENCE_PATTERNS = [
538
+ { start: "```tool_call", end: "```", reconstructStart: "```tool_call\n" },
539
+ { start: "```tool-call", end: "```", reconstructStart: "```tool-call\n" }
540
+ ];
541
+ var EXTENDED_FENCE_PATTERNS = [
542
+ ...DEFAULT_FENCE_PATTERNS,
543
+ {
544
+ start: "<tool_call>",
545
+ end: "</tool_call>",
546
+ reconstructStart: "<tool_call>"
547
+ }
548
+ ];
549
+ var ToolCallFenceDetector = class {
550
+ constructor(options = {}) {
551
+ this.pythonStyleRegex = /\[(\w+)\(/g;
552
+ this.buffer = "";
553
+ this.inFence = false;
554
+ this.fenceStartBuffer = "";
555
+ // Accumulated fence content
556
+ this.currentFencePattern = null;
557
+ this.fencePatterns = options.patterns ?? EXTENDED_FENCE_PATTERNS;
558
+ this.enablePythonStyle = options.enablePythonStyle ?? true;
559
+ this.fenceStarts = this.fencePatterns.map((p) => p.start);
434
560
  }
435
- const lines = results.map((result) => formatSingleToolResult(result)).join("\n");
436
- return `\`\`\`tool_result
437
- ${lines}
438
- \`\`\``;
439
- }
440
- function formatSingleToolResult(result) {
441
- return JSON.stringify({
442
- id: result.toolCallId,
443
- name: result.toolName,
444
- result: result.result,
445
- error: result.isError ?? false
446
- });
447
- }
561
+ addChunk(chunk) {
562
+ this.buffer += chunk;
563
+ }
564
+ getBuffer() {
565
+ return this.buffer;
566
+ }
567
+ clearBuffer() {
568
+ this.buffer = "";
569
+ }
570
+ /**
571
+ * Detects if there's a complete fence in the buffer
572
+ * @returns Detection result with fence info and safe text
573
+ */
574
+ detectFence() {
575
+ const {
576
+ index: startIdx,
577
+ prefix: matchedPrefix,
578
+ pattern
579
+ } = this.findFenceStart(this.buffer);
580
+ if (startIdx === -1) {
581
+ const overlap = this.computeOverlapLength(this.buffer, this.fenceStarts);
582
+ const safeTextLength = this.buffer.length - overlap;
583
+ const prefixText2 = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
584
+ const remaining = overlap > 0 ? this.buffer.slice(-overlap) : "";
585
+ this.buffer = remaining;
586
+ return {
587
+ fence: null,
588
+ prefixText: prefixText2,
589
+ remainingText: "",
590
+ overlapLength: overlap
591
+ };
592
+ }
593
+ const prefixText = this.buffer.slice(0, startIdx);
594
+ this.buffer = this.buffer.slice(startIdx);
595
+ const prefixLength = matchedPrefix?.length ?? 0;
596
+ const fenceEnd = pattern?.end ?? "```";
597
+ const closingIdx = this.buffer.indexOf(fenceEnd, prefixLength);
598
+ if (closingIdx === -1) {
599
+ return {
600
+ fence: null,
601
+ prefixText,
602
+ remainingText: "",
603
+ overlapLength: 0
604
+ };
605
+ }
606
+ const endPos = closingIdx + fenceEnd.length;
607
+ const fence = this.buffer.slice(0, endPos);
608
+ const remainingText = this.buffer.slice(endPos);
609
+ this.buffer = "";
610
+ return {
611
+ fence,
612
+ prefixText,
613
+ remainingText,
614
+ overlapLength: 0
615
+ };
616
+ }
617
+ /**
618
+ * Finds the first occurrence of any fence start marker
619
+ *
620
+ * @param text - Text to search in
621
+ * @returns Index of first fence start and which pattern matched
622
+ * @private
623
+ */
624
+ findFenceStart(text) {
625
+ let bestIndex = -1;
626
+ let matchedPrefix = null;
627
+ let matchedPattern = null;
628
+ for (const pattern of this.fencePatterns) {
629
+ const idx = text.indexOf(pattern.start);
630
+ if (idx !== -1 && (bestIndex === -1 || idx < bestIndex)) {
631
+ bestIndex = idx;
632
+ matchedPrefix = pattern.start;
633
+ matchedPattern = pattern;
634
+ }
635
+ }
636
+ if (this.enablePythonStyle) {
637
+ this.pythonStyleRegex.lastIndex = 0;
638
+ const pythonMatch = this.pythonStyleRegex.exec(text);
639
+ if (pythonMatch && (bestIndex === -1 || pythonMatch.index < bestIndex)) {
640
+ bestIndex = pythonMatch.index;
641
+ matchedPrefix = pythonMatch[0];
642
+ matchedPattern = {
643
+ start: pythonMatch[0],
644
+ end: ")]",
645
+ reconstructStart: pythonMatch[0],
646
+ isRegex: true
647
+ };
648
+ }
649
+ }
650
+ return { index: bestIndex, prefix: matchedPrefix, pattern: matchedPattern };
651
+ }
652
+ /**
653
+ * Computes the maximum overlap between the end of text and the start of any prefix
654
+ * @param text - Text to check for overlap
655
+ * @param prefixes - List of prefixes to check against
656
+ * @returns Length of the maximum overlap found
657
+ */
658
+ computeOverlapLength(text, prefixes) {
659
+ let overlap = 0;
660
+ for (const prefix of prefixes) {
661
+ const maxLength = Math.min(text.length, prefix.length - 1);
662
+ for (let size = maxLength; size > 0; size -= 1) {
663
+ if (prefix.startsWith(text.slice(-size))) {
664
+ overlap = Math.max(overlap, size);
665
+ break;
666
+ }
667
+ }
668
+ }
669
+ return overlap;
670
+ }
671
+ /**
672
+ * Checks if the buffer currently contains any text
673
+ */
674
+ hasContent() {
675
+ return this.buffer.length > 0;
676
+ }
677
+ /**
678
+ * Gets the buffer size
679
+ */
680
+ getBufferSize() {
681
+ return this.buffer.length;
682
+ }
683
+ /**
684
+ * Detect and stream fence content in real-time for true incremental streaming
685
+ * @returns Streaming result with current state and safe content to emit
686
+ */
687
+ detectStreamingFence() {
688
+ if (!this.inFence) {
689
+ const {
690
+ index: startIdx,
691
+ prefix: matchedPrefix,
692
+ pattern
693
+ } = this.findFenceStart(this.buffer);
694
+ if (startIdx === -1) {
695
+ const overlap = this.computeOverlapLength(
696
+ this.buffer,
697
+ this.fenceStarts
698
+ );
699
+ const safeTextLength = this.buffer.length - overlap;
700
+ const safeContent = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
701
+ this.buffer = this.buffer.slice(safeTextLength);
702
+ return {
703
+ inFence: false,
704
+ safeContent,
705
+ completeFence: null,
706
+ textAfterFence: ""
707
+ };
708
+ }
709
+ const prefixText = this.buffer.slice(0, startIdx);
710
+ const fenceStartLength = matchedPrefix?.length ?? 0;
711
+ this.buffer = this.buffer.slice(startIdx + fenceStartLength);
712
+ if (pattern && pattern.start.startsWith("```") && this.buffer.startsWith("\n")) {
713
+ this.buffer = this.buffer.slice(1);
714
+ }
715
+ this.inFence = true;
716
+ this.fenceStartBuffer = "";
717
+ this.currentFencePattern = pattern;
718
+ return {
719
+ inFence: true,
720
+ safeContent: prefixText,
721
+ // Emit any text before the fence
722
+ completeFence: null,
723
+ textAfterFence: ""
724
+ };
725
+ }
726
+ const fenceEnd = this.currentFencePattern?.end ?? "```";
727
+ const closingIdx = this.buffer.indexOf(fenceEnd);
728
+ if (closingIdx === -1) {
729
+ const overlap = this.computeOverlapLength(this.buffer, [fenceEnd]);
730
+ const safeContentLength = this.buffer.length - overlap;
731
+ if (safeContentLength > 0) {
732
+ const safeContent = this.buffer.slice(0, safeContentLength);
733
+ this.fenceStartBuffer += safeContent;
734
+ this.buffer = this.buffer.slice(safeContentLength);
735
+ return {
736
+ inFence: true,
737
+ safeContent,
738
+ completeFence: null,
739
+ textAfterFence: ""
740
+ };
741
+ }
742
+ return {
743
+ inFence: true,
744
+ safeContent: "",
745
+ completeFence: null,
746
+ textAfterFence: ""
747
+ };
748
+ }
749
+ const fenceContent = this.buffer.slice(0, closingIdx);
750
+ this.fenceStartBuffer += fenceContent;
751
+ const reconstructStart = this.currentFencePattern?.reconstructStart ?? "```tool_call\n";
752
+ const completeFence = `${reconstructStart}${this.fenceStartBuffer}${fenceEnd}`;
753
+ const textAfterFence = this.buffer.slice(closingIdx + fenceEnd.length);
754
+ this.inFence = false;
755
+ this.fenceStartBuffer = "";
756
+ this.currentFencePattern = null;
757
+ this.buffer = textAfterFence;
758
+ return {
759
+ inFence: false,
760
+ safeContent: fenceContent,
761
+ // Emit the last bit of fence content
762
+ completeFence,
763
+ textAfterFence
764
+ };
765
+ }
766
+ isInFence() {
767
+ return this.inFence;
768
+ }
769
+ resetStreamingState() {
770
+ this.inFence = false;
771
+ this.fenceStartBuffer = "";
772
+ this.currentFencePattern = null;
773
+ }
774
+ };
448
775
 
449
776
  // src/convert-to-webllm-messages.tsx
450
777
  function convertToolResultOutput(output) {
@@ -591,27 +918,6 @@ import {
591
918
  MLCEngine
592
919
  } from "@mlc-ai/web-llm";
593
920
 
594
- // src/utils/warnings.ts
595
- function createUnsupportedSettingWarning(feature, details) {
596
- return {
597
- type: "unsupported",
598
- feature,
599
- details
600
- };
601
- }
602
- function createUnsupportedToolWarning(tool, details) {
603
- return {
604
- type: "unsupported",
605
- feature: `tool:${tool.name}`,
606
- details
607
- };
608
- }
609
-
610
- // src/utils/tool-utils.ts
611
- function isFunctionTool(tool) {
612
- return tool.type === "function";
613
- }
614
-
615
921
  // src/utils/prompt-utils.ts
616
922
  function extractSystemPrompt(messages) {
617
923
  const systemMessages = messages.filter((msg) => msg.role === "system");
@@ -651,206 +957,6 @@ ${existingContent}` : "")
651
957
  ];
652
958
  }
653
959
 
654
- // src/streaming/tool-call-detector.ts
655
- var ToolCallFenceDetector = class {
656
- constructor() {
657
- this.FENCE_STARTS = ["```tool_call"];
658
- this.FENCE_END = "```";
659
- this.buffer = "";
660
- this.inFence = false;
661
- this.fenceStartBuffer = "";
662
- }
663
- addChunk(chunk) {
664
- this.buffer += chunk;
665
- }
666
- getBuffer() {
667
- return this.buffer;
668
- }
669
- clearBuffer() {
670
- this.buffer = "";
671
- }
672
- /**
673
- * Detects if there's a complete fence in the buffer
674
- *
675
- * 1. Searches for fence start markers
676
- * 2. If found, looks for closing fence
677
- * 3. Computes overlap for partial fences
678
- * 4. Returns safe text that can be emitted
679
- *
680
- * @returns Detection result with fence info and safe text
681
- */
682
- detectFence() {
683
- const { index: startIdx, prefix: matchedPrefix } = this.findFenceStart(
684
- this.buffer
685
- );
686
- if (startIdx === -1) {
687
- const overlap = this.computeOverlapLength(this.buffer, this.FENCE_STARTS);
688
- const safeTextLength = this.buffer.length - overlap;
689
- const prefixText2 = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
690
- const remaining = overlap > 0 ? this.buffer.slice(-overlap) : "";
691
- this.buffer = remaining;
692
- return {
693
- fence: null,
694
- prefixText: prefixText2,
695
- remainingText: "",
696
- overlapLength: overlap
697
- };
698
- }
699
- const prefixText = this.buffer.slice(0, startIdx);
700
- this.buffer = this.buffer.slice(startIdx);
701
- const prefixLength = matchedPrefix?.length ?? 0;
702
- const closingIdx = this.buffer.indexOf(this.FENCE_END, prefixLength);
703
- if (closingIdx === -1) {
704
- return {
705
- fence: null,
706
- prefixText,
707
- remainingText: "",
708
- overlapLength: 0
709
- };
710
- }
711
- const endPos = closingIdx + this.FENCE_END.length;
712
- const fence = this.buffer.slice(0, endPos);
713
- const remainingText = this.buffer.slice(endPos);
714
- this.buffer = "";
715
- return {
716
- fence,
717
- prefixText,
718
- remainingText,
719
- overlapLength: 0
720
- };
721
- }
722
- /**
723
- * Finds the first occurrence of any fence start marker
724
- *
725
- * @param text - Text to search in
726
- * @returns Index of first fence start and which prefix matched
727
- * @private
728
- */
729
- findFenceStart(text) {
730
- let bestIndex = -1;
731
- let matchedPrefix = null;
732
- for (const prefix of this.FENCE_STARTS) {
733
- const idx = text.indexOf(prefix);
734
- if (idx !== -1 && (bestIndex === -1 || idx < bestIndex)) {
735
- bestIndex = idx;
736
- matchedPrefix = prefix;
737
- }
738
- }
739
- return { index: bestIndex, prefix: matchedPrefix };
740
- }
741
- computeOverlapLength(text, prefixes) {
742
- let overlap = 0;
743
- for (const prefix of prefixes) {
744
- const maxLength = Math.min(text.length, prefix.length - 1);
745
- for (let size = maxLength; size > 0; size -= 1) {
746
- if (prefix.startsWith(text.slice(-size))) {
747
- overlap = Math.max(overlap, size);
748
- break;
749
- }
750
- }
751
- }
752
- return overlap;
753
- }
754
- hasContent() {
755
- return this.buffer.length > 0;
756
- }
757
- getBufferSize() {
758
- return this.buffer.length;
759
- }
760
- /**
761
- * Detect and stream fence content in real-time for true incremental streaming
762
- *
763
- * This method is designed for streaming tool calls as they arrive:
764
- * 1. Detects when a fence starts and transitions to "inFence" state
765
- * 2. While inFence, emits safe content that won't conflict with fence end marker
766
- * 3. When fence ends, returns the complete fence for parsing
767
- *
768
- * @returns Streaming result with current state and safe content to emit
769
- */
770
- detectStreamingFence() {
771
- if (!this.inFence) {
772
- const { index: startIdx, prefix: matchedPrefix } = this.findFenceStart(
773
- this.buffer
774
- );
775
- if (startIdx === -1) {
776
- const overlap = this.computeOverlapLength(
777
- this.buffer,
778
- this.FENCE_STARTS
779
- );
780
- const safeTextLength = this.buffer.length - overlap;
781
- const safeContent = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
782
- this.buffer = this.buffer.slice(safeTextLength);
783
- return {
784
- inFence: false,
785
- safeContent,
786
- completeFence: null,
787
- textAfterFence: ""
788
- };
789
- }
790
- const prefixText = this.buffer.slice(0, startIdx);
791
- const fenceStartLength = matchedPrefix?.length ?? 0;
792
- this.buffer = this.buffer.slice(startIdx + fenceStartLength);
793
- if (this.buffer.startsWith("\n")) {
794
- this.buffer = this.buffer.slice(1);
795
- }
796
- this.inFence = true;
797
- this.fenceStartBuffer = "";
798
- return {
799
- inFence: true,
800
- safeContent: prefixText,
801
- completeFence: null,
802
- textAfterFence: ""
803
- };
804
- }
805
- const closingIdx = this.buffer.indexOf(this.FENCE_END);
806
- if (closingIdx === -1) {
807
- const overlap = this.computeOverlapLength(this.buffer, [this.FENCE_END]);
808
- const safeContentLength = this.buffer.length - overlap;
809
- if (safeContentLength > 0) {
810
- const safeContent = this.buffer.slice(0, safeContentLength);
811
- this.fenceStartBuffer += safeContent;
812
- this.buffer = this.buffer.slice(safeContentLength);
813
- return {
814
- inFence: true,
815
- safeContent,
816
- completeFence: null,
817
- textAfterFence: ""
818
- };
819
- }
820
- return {
821
- inFence: true,
822
- safeContent: "",
823
- completeFence: null,
824
- textAfterFence: ""
825
- };
826
- }
827
- const fenceContent = this.buffer.slice(0, closingIdx);
828
- this.fenceStartBuffer += fenceContent;
829
- const completeFence = `${this.FENCE_STARTS[0]}
830
- ${this.fenceStartBuffer}
831
- ${this.FENCE_END}`;
832
- const textAfterFence = this.buffer.slice(
833
- closingIdx + this.FENCE_END.length
834
- );
835
- this.inFence = false;
836
- this.fenceStartBuffer = "";
837
- this.buffer = textAfterFence;
838
- return {
839
- inFence: false,
840
- safeContent: fenceContent,
841
- completeFence,
842
- textAfterFence
843
- };
844
- }
845
- isInFence() {
846
- return this.inFence;
847
- }
848
- resetStreamingState() {
849
- this.inFence = false;
850
- this.fenceStartBuffer = "";
851
- }
852
- };
853
-
854
960
  // src/web-llm-language-model.ts
855
961
  function isMobile() {
856
962
  if (typeof navigator === "undefined") return false;