@browser-ai/web-llm 2.1.4 → 2.1.6
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.js +206 -197
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +206 -197
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -889,7 +889,182 @@ function extractArgumentsDelta(content, state) {
|
|
|
889
889
|
return delta;
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
-
// src/
|
|
892
|
+
// ../shared/src/streaming/stream-processor.ts
|
|
893
|
+
function generateToolCallId2() {
|
|
894
|
+
return `call_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
895
|
+
}
|
|
896
|
+
async function processToolCallStream(chunks, emitTextDelta, controller, options) {
|
|
897
|
+
const fenceDetector = new ToolCallFenceDetector();
|
|
898
|
+
let currentToolCallId = null;
|
|
899
|
+
let toolInputStartEmitted = false;
|
|
900
|
+
let accumulatedFenceContent = "";
|
|
901
|
+
let argumentsStreamState = createArgumentsStreamState();
|
|
902
|
+
let insideFence = false;
|
|
903
|
+
let toolCallDetected = false;
|
|
904
|
+
let toolCalls = [];
|
|
905
|
+
let trailingText = "";
|
|
906
|
+
const resetFenceState = () => {
|
|
907
|
+
currentToolCallId = null;
|
|
908
|
+
toolInputStartEmitted = false;
|
|
909
|
+
accumulatedFenceContent = "";
|
|
910
|
+
argumentsStreamState = createArgumentsStreamState();
|
|
911
|
+
insideFence = false;
|
|
912
|
+
};
|
|
913
|
+
for await (const chunk of chunks) {
|
|
914
|
+
if (toolCallDetected) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
fenceDetector.addChunk(chunk);
|
|
918
|
+
while (fenceDetector.hasContent()) {
|
|
919
|
+
const wasInsideFence = insideFence;
|
|
920
|
+
const result = fenceDetector.detectStreamingFence();
|
|
921
|
+
insideFence = result.inFence;
|
|
922
|
+
let madeProgress = false;
|
|
923
|
+
if (!wasInsideFence && result.inFence) {
|
|
924
|
+
if (result.safeContent) {
|
|
925
|
+
emitTextDelta(result.safeContent);
|
|
926
|
+
madeProgress = true;
|
|
927
|
+
}
|
|
928
|
+
currentToolCallId = generateToolCallId2();
|
|
929
|
+
toolInputStartEmitted = false;
|
|
930
|
+
accumulatedFenceContent = "";
|
|
931
|
+
argumentsStreamState = createArgumentsStreamState();
|
|
932
|
+
insideFence = true;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
if (result.completeFence) {
|
|
936
|
+
madeProgress = true;
|
|
937
|
+
if (result.safeContent) {
|
|
938
|
+
accumulatedFenceContent += result.safeContent;
|
|
939
|
+
}
|
|
940
|
+
if (toolInputStartEmitted && currentToolCallId) {
|
|
941
|
+
const delta = extractArgumentsDelta(
|
|
942
|
+
accumulatedFenceContent,
|
|
943
|
+
argumentsStreamState
|
|
944
|
+
);
|
|
945
|
+
if (delta.length > 0) {
|
|
946
|
+
controller.enqueue({
|
|
947
|
+
type: "tool-input-delta",
|
|
948
|
+
id: currentToolCallId,
|
|
949
|
+
delta
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const parsed = parseJsonFunctionCalls(result.completeFence);
|
|
954
|
+
const selectedToolCalls = parsed.toolCalls.slice(0, 1);
|
|
955
|
+
if (selectedToolCalls.length === 0) {
|
|
956
|
+
emitTextDelta(result.completeFence);
|
|
957
|
+
if (result.textAfterFence) {
|
|
958
|
+
emitTextDelta(result.textAfterFence);
|
|
959
|
+
}
|
|
960
|
+
resetFenceState();
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
if (currentToolCallId) {
|
|
964
|
+
selectedToolCalls[0].toolCallId = currentToolCallId;
|
|
965
|
+
}
|
|
966
|
+
for (const [index, call] of selectedToolCalls.entries()) {
|
|
967
|
+
const toolCallId = index === 0 && currentToolCallId ? currentToolCallId : call.toolCallId;
|
|
968
|
+
const toolName = call.toolName;
|
|
969
|
+
const argsJson = JSON.stringify(call.args ?? {});
|
|
970
|
+
if (toolCallId === currentToolCallId) {
|
|
971
|
+
if (!toolInputStartEmitted) {
|
|
972
|
+
controller.enqueue({
|
|
973
|
+
type: "tool-input-start",
|
|
974
|
+
id: toolCallId,
|
|
975
|
+
toolName
|
|
976
|
+
});
|
|
977
|
+
toolInputStartEmitted = true;
|
|
978
|
+
}
|
|
979
|
+
const delta = extractArgumentsDelta(
|
|
980
|
+
accumulatedFenceContent,
|
|
981
|
+
argumentsStreamState
|
|
982
|
+
);
|
|
983
|
+
if (delta.length > 0) {
|
|
984
|
+
controller.enqueue({
|
|
985
|
+
type: "tool-input-delta",
|
|
986
|
+
id: toolCallId,
|
|
987
|
+
delta
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
} else {
|
|
991
|
+
controller.enqueue({
|
|
992
|
+
type: "tool-input-start",
|
|
993
|
+
id: toolCallId,
|
|
994
|
+
toolName
|
|
995
|
+
});
|
|
996
|
+
if (argsJson.length > 0) {
|
|
997
|
+
controller.enqueue({
|
|
998
|
+
type: "tool-input-delta",
|
|
999
|
+
id: toolCallId,
|
|
1000
|
+
delta: argsJson
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
controller.enqueue({ type: "tool-input-end", id: toolCallId });
|
|
1005
|
+
controller.enqueue({
|
|
1006
|
+
type: "tool-call",
|
|
1007
|
+
toolCallId,
|
|
1008
|
+
toolName,
|
|
1009
|
+
input: argsJson,
|
|
1010
|
+
providerExecuted: false
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
trailingText = result.textAfterFence ?? "";
|
|
1014
|
+
toolCalls = selectedToolCalls;
|
|
1015
|
+
toolCallDetected = true;
|
|
1016
|
+
resetFenceState();
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
if (insideFence) {
|
|
1020
|
+
if (result.safeContent) {
|
|
1021
|
+
accumulatedFenceContent += result.safeContent;
|
|
1022
|
+
madeProgress = true;
|
|
1023
|
+
const toolName = extractToolName(accumulatedFenceContent);
|
|
1024
|
+
if (toolName && !toolInputStartEmitted && currentToolCallId) {
|
|
1025
|
+
controller.enqueue({
|
|
1026
|
+
type: "tool-input-start",
|
|
1027
|
+
id: currentToolCallId,
|
|
1028
|
+
toolName
|
|
1029
|
+
});
|
|
1030
|
+
toolInputStartEmitted = true;
|
|
1031
|
+
}
|
|
1032
|
+
if (toolInputStartEmitted && currentToolCallId) {
|
|
1033
|
+
const delta = extractArgumentsDelta(
|
|
1034
|
+
accumulatedFenceContent,
|
|
1035
|
+
argumentsStreamState
|
|
1036
|
+
);
|
|
1037
|
+
if (delta.length > 0) {
|
|
1038
|
+
controller.enqueue({
|
|
1039
|
+
type: "tool-input-delta",
|
|
1040
|
+
id: currentToolCallId,
|
|
1041
|
+
delta
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (!insideFence && result.safeContent) {
|
|
1049
|
+
emitTextDelta(result.safeContent);
|
|
1050
|
+
madeProgress = true;
|
|
1051
|
+
}
|
|
1052
|
+
if (!madeProgress) {
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (toolCallDetected && options?.stopEarlyOnToolCall) {
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (!toolCallDetected && fenceDetector.hasContent()) {
|
|
1061
|
+
emitTextDelta(fenceDetector.getBuffer());
|
|
1062
|
+
fenceDetector.clearBuffer();
|
|
1063
|
+
}
|
|
1064
|
+
return { toolCallDetected, toolCalls, trailingText };
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/utils/convert-to-webllm-messages.tsx
|
|
893
1068
|
function convertToolResultOutput(output) {
|
|
894
1069
|
switch (output.type) {
|
|
895
1070
|
case "text":
|
|
@@ -1028,7 +1203,7 @@ function convertToWebLLMMessages(prompt) {
|
|
|
1028
1203
|
return messages;
|
|
1029
1204
|
}
|
|
1030
1205
|
|
|
1031
|
-
// src/web-llm-language-model.ts
|
|
1206
|
+
// src/chat/web-llm-language-model.ts
|
|
1032
1207
|
var import_web_llm = require("@mlc-ai/web-llm");
|
|
1033
1208
|
|
|
1034
1209
|
// src/utils/prompt-utils.ts
|
|
@@ -1088,7 +1263,7 @@ function doesBrowserSupportWebLLM() {
|
|
|
1088
1263
|
return checkWebGPU();
|
|
1089
1264
|
}
|
|
1090
1265
|
|
|
1091
|
-
// src/web-llm-language-model.ts
|
|
1266
|
+
// src/chat/web-llm-language-model.ts
|
|
1092
1267
|
var WebLLMLanguageModel = class {
|
|
1093
1268
|
constructor(modelId, options = {}) {
|
|
1094
1269
|
this.specificationVersion = "v3";
|
|
@@ -1242,10 +1417,12 @@ var WebLLMLanguageModel = class {
|
|
|
1242
1417
|
top_p: topP,
|
|
1243
1418
|
seed
|
|
1244
1419
|
};
|
|
1245
|
-
|
|
1420
|
+
const webLLMOptions = providerOptions?.[this.provider];
|
|
1421
|
+
const extraBody = webLLMOptions?.extra_body;
|
|
1422
|
+
if (extraBody) {
|
|
1246
1423
|
requestOptions.extra_body = {
|
|
1247
|
-
enable_thinking:
|
|
1248
|
-
enable_latency_breakdown:
|
|
1424
|
+
enable_thinking: extraBody.enable_thinking,
|
|
1425
|
+
enable_latency_breakdown: extraBody.enable_latency_breakdown
|
|
1249
1426
|
};
|
|
1250
1427
|
}
|
|
1251
1428
|
if (responseFormat?.type === "json") {
|
|
@@ -1341,7 +1518,7 @@ var WebLLMLanguageModel = class {
|
|
|
1341
1518
|
reasoning: void 0
|
|
1342
1519
|
}
|
|
1343
1520
|
},
|
|
1344
|
-
request: { body: { messages: promptMessages
|
|
1521
|
+
request: { body: { ...requestOptions, messages: promptMessages } },
|
|
1345
1522
|
warnings
|
|
1346
1523
|
};
|
|
1347
1524
|
}
|
|
@@ -1377,7 +1554,7 @@ var WebLLMLanguageModel = class {
|
|
|
1377
1554
|
total: response.usage?.total_tokens
|
|
1378
1555
|
}
|
|
1379
1556
|
},
|
|
1380
|
-
request: { body: { messages: promptMessages
|
|
1557
|
+
request: { body: { ...requestOptions, messages: promptMessages } },
|
|
1381
1558
|
warnings
|
|
1382
1559
|
};
|
|
1383
1560
|
} catch (error) {
|
|
@@ -1532,195 +1709,27 @@ var WebLLMLanguageModel = class {
|
|
|
1532
1709
|
...options.abortSignal && !useWorker && { signal: options.abortSignal }
|
|
1533
1710
|
};
|
|
1534
1711
|
const response = await engine.chat.completions.create(streamingRequest);
|
|
1535
|
-
|
|
1536
|
-
let
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
if (!choice) continue;
|
|
1545
|
-
if (choice.delta.content) {
|
|
1546
|
-
const delta = choice.delta.content;
|
|
1547
|
-
accumulatedText += delta;
|
|
1548
|
-
fenceDetector.addChunk(delta);
|
|
1549
|
-
while (fenceDetector.hasContent()) {
|
|
1550
|
-
const wasInsideFence = insideFence;
|
|
1551
|
-
const result = fenceDetector.detectStreamingFence();
|
|
1552
|
-
insideFence = result.inFence;
|
|
1553
|
-
let madeProgress = false;
|
|
1554
|
-
if (!wasInsideFence && result.inFence) {
|
|
1555
|
-
if (result.safeContent) {
|
|
1556
|
-
emitTextDelta(result.safeContent);
|
|
1557
|
-
madeProgress = true;
|
|
1558
|
-
}
|
|
1559
|
-
currentToolCallId = `call_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
1560
|
-
toolInputStartEmitted = false;
|
|
1561
|
-
accumulatedFenceContent = "";
|
|
1562
|
-
argumentsStreamState = createArgumentsStreamState();
|
|
1563
|
-
insideFence = true;
|
|
1564
|
-
continue;
|
|
1565
|
-
}
|
|
1566
|
-
if (result.completeFence) {
|
|
1567
|
-
madeProgress = true;
|
|
1568
|
-
if (result.safeContent) {
|
|
1569
|
-
accumulatedFenceContent += result.safeContent;
|
|
1570
|
-
}
|
|
1571
|
-
if (toolInputStartEmitted && currentToolCallId) {
|
|
1572
|
-
const delta2 = extractArgumentsDelta(
|
|
1573
|
-
accumulatedFenceContent,
|
|
1574
|
-
argumentsStreamState
|
|
1575
|
-
);
|
|
1576
|
-
if (delta2.length > 0) {
|
|
1577
|
-
controller.enqueue({
|
|
1578
|
-
type: "tool-input-delta",
|
|
1579
|
-
id: currentToolCallId,
|
|
1580
|
-
delta: delta2
|
|
1581
|
-
});
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
const parsed = parseJsonFunctionCalls(result.completeFence);
|
|
1585
|
-
const parsedToolCalls = parsed.toolCalls;
|
|
1586
|
-
const selectedToolCalls = parsedToolCalls.slice(0, 1);
|
|
1587
|
-
if (selectedToolCalls.length === 0) {
|
|
1588
|
-
emitTextDelta(result.completeFence);
|
|
1589
|
-
if (result.textAfterFence) {
|
|
1590
|
-
emitTextDelta(result.textAfterFence);
|
|
1591
|
-
}
|
|
1592
|
-
currentToolCallId = null;
|
|
1593
|
-
toolInputStartEmitted = false;
|
|
1594
|
-
accumulatedFenceContent = "";
|
|
1595
|
-
argumentsStreamState = createArgumentsStreamState();
|
|
1596
|
-
insideFence = false;
|
|
1597
|
-
continue;
|
|
1598
|
-
}
|
|
1599
|
-
if (selectedToolCalls.length > 0 && currentToolCallId) {
|
|
1600
|
-
selectedToolCalls[0].toolCallId = currentToolCallId;
|
|
1601
|
-
}
|
|
1602
|
-
for (const [index, call] of selectedToolCalls.entries()) {
|
|
1603
|
-
const toolCallId = index === 0 && currentToolCallId ? currentToolCallId : call.toolCallId;
|
|
1604
|
-
const toolName = call.toolName;
|
|
1605
|
-
const argsJson = JSON.stringify(call.args ?? {});
|
|
1606
|
-
if (toolCallId === currentToolCallId) {
|
|
1607
|
-
if (!toolInputStartEmitted) {
|
|
1608
|
-
controller.enqueue({
|
|
1609
|
-
type: "tool-input-start",
|
|
1610
|
-
id: toolCallId,
|
|
1611
|
-
toolName
|
|
1612
|
-
});
|
|
1613
|
-
toolInputStartEmitted = true;
|
|
1614
|
-
}
|
|
1615
|
-
const delta2 = extractArgumentsDelta(
|
|
1616
|
-
accumulatedFenceContent,
|
|
1617
|
-
argumentsStreamState
|
|
1618
|
-
);
|
|
1619
|
-
if (delta2.length > 0) {
|
|
1620
|
-
controller.enqueue({
|
|
1621
|
-
type: "tool-input-delta",
|
|
1622
|
-
id: toolCallId,
|
|
1623
|
-
delta: delta2
|
|
1624
|
-
});
|
|
1625
|
-
}
|
|
1626
|
-
} else {
|
|
1627
|
-
controller.enqueue({
|
|
1628
|
-
type: "tool-input-start",
|
|
1629
|
-
id: toolCallId,
|
|
1630
|
-
toolName
|
|
1631
|
-
});
|
|
1632
|
-
if (argsJson.length > 0) {
|
|
1633
|
-
controller.enqueue({
|
|
1634
|
-
type: "tool-input-delta",
|
|
1635
|
-
id: toolCallId,
|
|
1636
|
-
delta: argsJson
|
|
1637
|
-
});
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
controller.enqueue({
|
|
1641
|
-
type: "tool-input-end",
|
|
1642
|
-
id: toolCallId
|
|
1643
|
-
});
|
|
1644
|
-
controller.enqueue({
|
|
1645
|
-
type: "tool-call",
|
|
1646
|
-
toolCallId,
|
|
1647
|
-
toolName,
|
|
1648
|
-
input: argsJson,
|
|
1649
|
-
providerExecuted: false
|
|
1650
|
-
});
|
|
1651
|
-
}
|
|
1652
|
-
if (result.textAfterFence) {
|
|
1653
|
-
emitTextDelta(result.textAfterFence);
|
|
1654
|
-
}
|
|
1655
|
-
madeProgress = true;
|
|
1656
|
-
currentToolCallId = null;
|
|
1657
|
-
toolInputStartEmitted = false;
|
|
1658
|
-
accumulatedFenceContent = "";
|
|
1659
|
-
argumentsStreamState = createArgumentsStreamState();
|
|
1660
|
-
insideFence = false;
|
|
1661
|
-
continue;
|
|
1662
|
-
}
|
|
1663
|
-
if (insideFence) {
|
|
1664
|
-
if (result.safeContent) {
|
|
1665
|
-
accumulatedFenceContent += result.safeContent;
|
|
1666
|
-
madeProgress = true;
|
|
1667
|
-
const toolName = extractToolName(accumulatedFenceContent);
|
|
1668
|
-
if (toolName && !toolInputStartEmitted && currentToolCallId) {
|
|
1669
|
-
controller.enqueue({
|
|
1670
|
-
type: "tool-input-start",
|
|
1671
|
-
id: currentToolCallId,
|
|
1672
|
-
toolName
|
|
1673
|
-
});
|
|
1674
|
-
toolInputStartEmitted = true;
|
|
1675
|
-
}
|
|
1676
|
-
if (toolInputStartEmitted && currentToolCallId) {
|
|
1677
|
-
const delta2 = extractArgumentsDelta(
|
|
1678
|
-
accumulatedFenceContent,
|
|
1679
|
-
argumentsStreamState
|
|
1680
|
-
);
|
|
1681
|
-
if (delta2.length > 0) {
|
|
1682
|
-
controller.enqueue({
|
|
1683
|
-
type: "tool-input-delta",
|
|
1684
|
-
id: currentToolCallId,
|
|
1685
|
-
delta: delta2
|
|
1686
|
-
});
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
continue;
|
|
1691
|
-
}
|
|
1692
|
-
if (!insideFence && result.safeContent) {
|
|
1693
|
-
emitTextDelta(result.safeContent);
|
|
1694
|
-
madeProgress = true;
|
|
1695
|
-
}
|
|
1696
|
-
if (!madeProgress) {
|
|
1697
|
-
break;
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1712
|
+
let lastUsage;
|
|
1713
|
+
let isAbort = false;
|
|
1714
|
+
const chunks = (async function* () {
|
|
1715
|
+
for await (const chunk of response) {
|
|
1716
|
+
const choice = chunk.choices[0];
|
|
1717
|
+
if (!choice) continue;
|
|
1718
|
+
if (choice.delta.content) yield choice.delta.content;
|
|
1719
|
+
if (chunk.usage) lastUsage = chunk.usage;
|
|
1720
|
+
if (choice.finish_reason === "abort") isAbort = true;
|
|
1700
1721
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
};
|
|
1710
|
-
if (choice.finish_reason === "abort") {
|
|
1711
|
-
finishReason = { unified: "other", raw: "abort" };
|
|
1712
|
-
} else {
|
|
1713
|
-
const { toolCalls } = parseJsonFunctionCalls(accumulatedText);
|
|
1714
|
-
if (toolCalls.length > 0) {
|
|
1715
|
-
finishReason = { unified: "tool-calls", raw: "tool-calls" };
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
finishStream(finishReason, chunk.usage);
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
if (!finished) {
|
|
1722
|
-
finishStream({ unified: "stop", raw: "stop" });
|
|
1722
|
+
})();
|
|
1723
|
+
const result = await processToolCallStream(
|
|
1724
|
+
chunks,
|
|
1725
|
+
emitTextDelta,
|
|
1726
|
+
controller
|
|
1727
|
+
);
|
|
1728
|
+
if (result.trailingText) {
|
|
1729
|
+
emitTextDelta(result.trailingText);
|
|
1723
1730
|
}
|
|
1731
|
+
const finishReason = isAbort ? { unified: "other", raw: "abort" } : result.toolCallDetected ? { unified: "tool-calls", raw: "tool-calls" } : { unified: "stop", raw: "stop" };
|
|
1732
|
+
finishStream(finishReason, lastUsage);
|
|
1724
1733
|
} catch (error) {
|
|
1725
1734
|
controller.error(error);
|
|
1726
1735
|
} finally {
|
|
@@ -1735,12 +1744,12 @@ var WebLLMLanguageModel = class {
|
|
|
1735
1744
|
});
|
|
1736
1745
|
return {
|
|
1737
1746
|
stream,
|
|
1738
|
-
request: { body: { messages: promptMessages
|
|
1747
|
+
request: { body: { ...requestOptions, messages: promptMessages } }
|
|
1739
1748
|
};
|
|
1740
1749
|
}
|
|
1741
1750
|
};
|
|
1742
1751
|
|
|
1743
|
-
// src/web-llm-embedding-model.ts
|
|
1752
|
+
// src/embedding/web-llm-embedding-model.ts
|
|
1744
1753
|
var import_web_llm2 = require("@mlc-ai/web-llm");
|
|
1745
1754
|
var WebLLMEmbeddingModel = class {
|
|
1746
1755
|
constructor(modelId, options = {}) {
|