llm_meta_widget 0.12.0 → 0.12.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +1 -1
- data/app/assets/javascripts/llm_meta_widget/element.js +17 -12
- data/app/assets/javascripts/llm_meta_widget/llm-meta-widget.js +26 -17
- data/app/assets/javascripts/llm_meta_widget/orchestrator.js +39 -8
- data/lib/llm_meta_widget/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 845c38baf1aca6cc874a77c9f324895c6001664dffe4d891d8b31ed2057a3b2b
|
|
4
|
+
data.tar.gz: 825acda8a84c599e09e8d11cad9fe738092aa807b41911a99889e76dab15c800
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0244dc664c60f88e1cf29e9cb52ebc9e33bf61e8ab2ab180a8e0873d587bf1cf96d8865eeb9d97b0fe325cec00c43e6e154cef74c511f7f16894a2112d17ad23
|
|
7
|
+
data.tar.gz: 5584f49e2c7e848ef93fdb5bf6cb1270fa3154f77beee255eb64e221aab20ce8e4828b4b8381c5268e1503b1b0967d4fab81811be718f23f247951ccdf1c142a
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,35 @@
|
|
|
3
3
|
This file starts at 0.8.0, the first release with a breaking change worth
|
|
4
4
|
calling out. Earlier releases are in the git history (`git log --oneline v0.7.6`).
|
|
5
5
|
|
|
6
|
+
## 0.12.1
|
|
7
|
+
|
|
8
|
+
### Fixed: a tool result is counted as JSON, not as prose
|
|
9
|
+
|
|
10
|
+
The turn was sized at roughly four characters per token, which is right for
|
|
11
|
+
prose and badly wrong for a tool result. A request of 289,101 characters that
|
|
12
|
+
the provider counted as 135,291 tokens was estimated at 72,275 — 53% of its
|
|
13
|
+
real size — so nothing was given up and the provider refused the turn with its
|
|
14
|
+
own error text. That request was 33% letters, 16% digits and 45% punctuation:
|
|
15
|
+
JSON, which tokenizes about twice as densely as prose.
|
|
16
|
+
|
|
17
|
+
Characters are now counted in three classes — letters and spaces, CJK, and
|
|
18
|
+
everything else — which puts that request at 106% of its real size. Erring
|
|
19
|
+
high is deliberate here: underestimating ends the turn, while overestimating
|
|
20
|
+
gives up an old tool result slightly early. Tool schemas are counted the same
|
|
21
|
+
way, since a schema is JSON too.
|
|
22
|
+
|
|
23
|
+
### Fixed: following the output no longer fights a reader who scrolls
|
|
24
|
+
|
|
25
|
+
0.12.0 decided whether to follow the output from the `scroll` event. A browser
|
|
26
|
+
dispatches that event a frame after the position changes, while deltas arrive
|
|
27
|
+
every few milliseconds, so a delta could pin the view before the listener
|
|
28
|
+
heard that the reader had moved — and the pin put them back at the end, so the
|
|
29
|
+
decision never flipped. The question is now asked synchronously, immediately
|
|
30
|
+
before anything is added, which has no such window.
|
|
31
|
+
|
|
32
|
+
A separate report of the panel jumping to the TOP after a turn ends is not
|
|
33
|
+
explained by this and remains open.
|
|
34
|
+
|
|
6
35
|
## 0.12.0
|
|
7
36
|
|
|
8
37
|
Nothing a page must provide changes in this release: no new attribute, no
|
data/README.md
CHANGED
|
@@ -168,15 +168,11 @@ function boot(cfg) {
|
|
|
168
168
|
// The reader's own scrolling says which they are doing: leave the bottom
|
|
169
169
|
// and we stop following; come back to it and we resume.
|
|
170
170
|
var SCROLL_PIN_SLACK = 40; // px from the end still counts as "at the end"
|
|
171
|
-
var followingOutput = true;
|
|
172
171
|
|
|
173
172
|
function atBottom() {
|
|
174
173
|
return historyEl.scrollHeight - historyEl.scrollTop - historyEl.clientHeight <= SCROLL_PIN_SLACK;
|
|
175
174
|
}
|
|
176
175
|
function pinToBottom() { historyEl.scrollTop = historyEl.scrollHeight; }
|
|
177
|
-
// Called wherever content is added mid-turn.
|
|
178
|
-
function pinIfFollowing() { if (followingOutput) pinToBottom(); }
|
|
179
|
-
historyEl.addEventListener("scroll", function() { followingOutput = atBottom(); });
|
|
180
176
|
var formEl = root.querySelector(".lmw-form");
|
|
181
177
|
var inputEl = root.querySelector(".lmw-input");
|
|
182
178
|
var clearBtn = root.querySelector(".lmw-clear");
|
|
@@ -935,6 +931,8 @@ function boot(cfg) {
|
|
|
935
931
|
var pendingTurnLabel = null;
|
|
936
932
|
|
|
937
933
|
function appendTurn(role, text, turnLabel) {
|
|
934
|
+
// Captured BEFORE anything is added: see atBottom.
|
|
935
|
+
var following = atBottom(); // a question always wins
|
|
938
936
|
// Class names mirror llm_meta_chat's chats/_message.html.erb —
|
|
939
937
|
// `.message.<role>`, `.message-role`, `.message-content` — so the
|
|
940
938
|
// shared conversation.css styles apply directly (see the <link>
|
|
@@ -973,7 +971,9 @@ function boot(cfg) {
|
|
|
973
971
|
div.appendChild(document.createTextNode(" "));
|
|
974
972
|
div.appendChild(body);
|
|
975
973
|
historyEl.appendChild(div);
|
|
976
|
-
|
|
974
|
+
// A question the visitor just asked always goes to the end; anything
|
|
975
|
+
// else respects where they were reading.
|
|
976
|
+
if (role === "user" || following) pinToBottom();
|
|
977
977
|
body.roleLabel = label; // so a turn in flight can show it is working
|
|
978
978
|
return body;
|
|
979
979
|
}
|
|
@@ -1061,6 +1061,8 @@ function boot(cfg) {
|
|
|
1061
1061
|
}
|
|
1062
1062
|
|
|
1063
1063
|
function announceToolCall(toolCall) {
|
|
1064
|
+
// Captured BEFORE anything is added: see atBottom.
|
|
1065
|
+
var following = atBottom();
|
|
1064
1066
|
var row = chipRow();
|
|
1065
1067
|
if (!row) return;
|
|
1066
1068
|
var chip = document.createElement("span");
|
|
@@ -1069,7 +1071,7 @@ function boot(cfg) {
|
|
|
1069
1071
|
chip.dataset.tool = toolCall.name;
|
|
1070
1072
|
row.appendChild(chip);
|
|
1071
1073
|
pendingChips.push(chip);
|
|
1072
|
-
|
|
1074
|
+
if (following) pinToBottom();
|
|
1073
1075
|
}
|
|
1074
1076
|
|
|
1075
1077
|
function resolveToolCall(outcome) {
|
|
@@ -1091,6 +1093,8 @@ function boot(cfg) {
|
|
|
1091
1093
|
var activeAssistantBody = null;
|
|
1092
1094
|
|
|
1093
1095
|
function ensureThinkingBlock() {
|
|
1096
|
+
// Captured BEFORE anything is added: see atBottom.
|
|
1097
|
+
var following = atBottom();
|
|
1094
1098
|
if (currentThinkingBody) return currentThinkingBody;
|
|
1095
1099
|
// Class names match llm_meta_chat's .message-thinking pattern so
|
|
1096
1100
|
// the shared conversation.css applies directly.
|
|
@@ -1117,7 +1121,7 @@ function boot(cfg) {
|
|
|
1117
1121
|
} else {
|
|
1118
1122
|
historyEl.appendChild(details);
|
|
1119
1123
|
}
|
|
1120
|
-
|
|
1124
|
+
if (following) pinToBottom();
|
|
1121
1125
|
currentThinkingBlock = details;
|
|
1122
1126
|
currentThinkingBody = body;
|
|
1123
1127
|
return body;
|
|
@@ -1266,9 +1270,6 @@ function boot(cfg) {
|
|
|
1266
1270
|
var userText = inputEl.value.trim();
|
|
1267
1271
|
if (!userText) return;
|
|
1268
1272
|
inputEl.value = "";
|
|
1269
|
-
// Asking something is a request to see the answer: whatever the reader was
|
|
1270
|
-
// looking at before, they are following the output again from here.
|
|
1271
|
-
followingOutput = true;
|
|
1272
1273
|
appendTurn("user", userText, pendingTurnLabel);
|
|
1273
1274
|
pendingTurnLabel = null;
|
|
1274
1275
|
|
|
@@ -1356,19 +1357,23 @@ function boot(cfg) {
|
|
|
1356
1357
|
collapseThinkingBlock();
|
|
1357
1358
|
},
|
|
1358
1359
|
onThinkingDelta: function(delta) {
|
|
1360
|
+
// Captured BEFORE anything is added: see atBottom.
|
|
1361
|
+
var following = atBottom();
|
|
1359
1362
|
var body = ensureThinkingBlock();
|
|
1360
1363
|
body.appendChild(document.createTextNode(delta));
|
|
1361
1364
|
body.scrollTop = body.scrollHeight;
|
|
1362
|
-
|
|
1365
|
+
if (following) pinToBottom();
|
|
1363
1366
|
},
|
|
1364
1367
|
onTextDelta: function(delta) {
|
|
1368
|
+
// Captured BEFORE anything is added: see atBottom.
|
|
1369
|
+
var following = atBottom();
|
|
1365
1370
|
markDone(assistantBody.roleLabel);
|
|
1366
1371
|
collapseThinkingBlock();
|
|
1367
1372
|
assistantMarkdown += delta;
|
|
1368
1373
|
renderMarkdownInto(assistantBody, assistantMarkdown);
|
|
1369
1374
|
liveTurn.content = assistantMarkdown;
|
|
1370
1375
|
persistSoon();
|
|
1371
|
-
|
|
1376
|
+
if (following) pinToBottom();
|
|
1372
1377
|
}
|
|
1373
1378
|
});
|
|
1374
1379
|
// The turn was recorded here until 0.11.1, after the loop returned — which
|
|
@@ -589,17 +589,29 @@ function parseSseFrame(raw) {
|
|
|
589
589
|
var STATIC_PRIMITIVES_META = "io.modelcontextprotocol/static-primitives";
|
|
590
590
|
var PAGE_CARD_MEDIA_TYPE = "application/mcp-static-primitives+json";
|
|
591
591
|
var RESOURCE_SHARE_OF_WINDOW = 0.0625;
|
|
592
|
+
var RESOURCE_BYTES_PER_TOKEN = 4;
|
|
592
593
|
function resourceBudgetBytes(window2, { floor = 8e3, cap = 64e3 } = {}) {
|
|
593
594
|
if (!window2 || !Number.isFinite(window2)) return floor;
|
|
594
|
-
const share = Math.round(window2 * RESOURCE_SHARE_OF_WINDOW *
|
|
595
|
+
const share = Math.round(window2 * RESOURCE_SHARE_OF_WINDOW * RESOURCE_BYTES_PER_TOKEN);
|
|
595
596
|
return Math.min(cap, Math.max(floor, share));
|
|
596
597
|
}
|
|
597
|
-
var
|
|
598
|
+
var PROSE_CHARS_PER_TOKEN = 3.5;
|
|
599
|
+
var DENSE_CHARS_PER_TOKEN = 1.6;
|
|
600
|
+
var CJK_CHARS_PER_TOKEN = 1;
|
|
598
601
|
var MESSAGE_OVERHEAD_TOKENS = 4;
|
|
602
|
+
var CJK_PATTERN = /[\u3000-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]/g;
|
|
603
|
+
var PROSE_PATTERN = /[A-Za-z \n\t]/g;
|
|
604
|
+
function estimateTextTokens(text) {
|
|
605
|
+
const str = typeof text === "string" ? text : String(text ?? "");
|
|
606
|
+
const cjk = (str.match(CJK_PATTERN) || []).length;
|
|
607
|
+
const prose = (str.match(PROSE_PATTERN) || []).length;
|
|
608
|
+
const dense = str.length - cjk - prose;
|
|
609
|
+
return Math.ceil(cjk / CJK_CHARS_PER_TOKEN + prose / PROSE_CHARS_PER_TOKEN + dense / DENSE_CHARS_PER_TOKEN);
|
|
610
|
+
}
|
|
599
611
|
function estimateTokens(messages) {
|
|
600
612
|
return (messages || []).reduce((sum, m) => {
|
|
601
613
|
const content = typeof m?.content === "string" ? m.content : JSON.stringify(m?.content ?? "");
|
|
602
|
-
return sum +
|
|
614
|
+
return sum + estimateTextTokens(content) + MESSAGE_OVERHEAD_TOKENS;
|
|
603
615
|
}, 0);
|
|
604
616
|
}
|
|
605
617
|
function estimateToolsTokens(tools) {
|
|
@@ -608,7 +620,7 @@ function estimateToolsTokens(tools) {
|
|
|
608
620
|
description: t?.description,
|
|
609
621
|
schema: t?.input_schema || t?.inputSchema
|
|
610
622
|
})).join("");
|
|
611
|
-
return
|
|
623
|
+
return estimateTextTokens(text);
|
|
612
624
|
}
|
|
613
625
|
function evictOldToolResults({ messages, window: window2, reserve = 0, dropped = [] }) {
|
|
614
626
|
if (!window2 || !Number.isFinite(window2)) return { messages, dropped };
|
|
@@ -3940,19 +3952,12 @@ function boot(cfg) {
|
|
|
3940
3952
|
var toggleBtn = document.getElementById("llm-meta-widget-toggle");
|
|
3941
3953
|
var historyEl = root.querySelector(".lmw-messages");
|
|
3942
3954
|
var SCROLL_PIN_SLACK = 40;
|
|
3943
|
-
var followingOutput = true;
|
|
3944
3955
|
function atBottom() {
|
|
3945
3956
|
return historyEl.scrollHeight - historyEl.scrollTop - historyEl.clientHeight <= SCROLL_PIN_SLACK;
|
|
3946
3957
|
}
|
|
3947
3958
|
function pinToBottom() {
|
|
3948
3959
|
historyEl.scrollTop = historyEl.scrollHeight;
|
|
3949
3960
|
}
|
|
3950
|
-
function pinIfFollowing() {
|
|
3951
|
-
if (followingOutput) pinToBottom();
|
|
3952
|
-
}
|
|
3953
|
-
historyEl.addEventListener("scroll", function() {
|
|
3954
|
-
followingOutput = atBottom();
|
|
3955
|
-
});
|
|
3956
3961
|
var formEl = root.querySelector(".lmw-form");
|
|
3957
3962
|
var inputEl = root.querySelector(".lmw-input");
|
|
3958
3963
|
var clearBtn = root.querySelector(".lmw-clear");
|
|
@@ -4505,6 +4510,7 @@ function boot(cfg) {
|
|
|
4505
4510
|
}
|
|
4506
4511
|
var pendingTurnLabel = null;
|
|
4507
4512
|
function appendTurn(role, text, turnLabel) {
|
|
4513
|
+
var following = atBottom();
|
|
4508
4514
|
var welcome = historyEl.querySelector(".lmw-welcome");
|
|
4509
4515
|
if (welcome) welcome.remove();
|
|
4510
4516
|
var div = document.createElement("div");
|
|
@@ -4531,7 +4537,7 @@ function boot(cfg) {
|
|
|
4531
4537
|
div.appendChild(document.createTextNode(" "));
|
|
4532
4538
|
div.appendChild(body);
|
|
4533
4539
|
historyEl.appendChild(div);
|
|
4534
|
-
|
|
4540
|
+
if (role === "user" || following) pinToBottom();
|
|
4535
4541
|
body.roleLabel = label;
|
|
4536
4542
|
return body;
|
|
4537
4543
|
}
|
|
@@ -4581,6 +4587,7 @@ function boot(cfg) {
|
|
|
4581
4587
|
return liveChips;
|
|
4582
4588
|
}
|
|
4583
4589
|
function announceToolCall(toolCall) {
|
|
4590
|
+
var following = atBottom();
|
|
4584
4591
|
var row = chipRow();
|
|
4585
4592
|
if (!row) return;
|
|
4586
4593
|
var chip = document.createElement("span");
|
|
@@ -4589,7 +4596,7 @@ function boot(cfg) {
|
|
|
4589
4596
|
chip.dataset.tool = toolCall.name;
|
|
4590
4597
|
row.appendChild(chip);
|
|
4591
4598
|
pendingChips.push(chip);
|
|
4592
|
-
|
|
4599
|
+
if (following) pinToBottom();
|
|
4593
4600
|
}
|
|
4594
4601
|
function resolveToolCall(outcome) {
|
|
4595
4602
|
var idx = pendingChips.findIndex(function(c) {
|
|
@@ -4605,6 +4612,7 @@ function boot(cfg) {
|
|
|
4605
4612
|
var currentThinkingBody = null;
|
|
4606
4613
|
var activeAssistantBody = null;
|
|
4607
4614
|
function ensureThinkingBlock() {
|
|
4615
|
+
var following = atBottom();
|
|
4608
4616
|
if (currentThinkingBody) return currentThinkingBody;
|
|
4609
4617
|
var details = document.createElement("details");
|
|
4610
4618
|
details.className = "message-thinking";
|
|
@@ -4629,7 +4637,7 @@ function boot(cfg) {
|
|
|
4629
4637
|
} else {
|
|
4630
4638
|
historyEl.appendChild(details);
|
|
4631
4639
|
}
|
|
4632
|
-
|
|
4640
|
+
if (following) pinToBottom();
|
|
4633
4641
|
currentThinkingBlock = details;
|
|
4634
4642
|
currentThinkingBody = body;
|
|
4635
4643
|
return body;
|
|
@@ -4746,7 +4754,6 @@ function boot(cfg) {
|
|
|
4746
4754
|
var userText = inputEl.value.trim();
|
|
4747
4755
|
if (!userText) return;
|
|
4748
4756
|
inputEl.value = "";
|
|
4749
|
-
followingOutput = true;
|
|
4750
4757
|
appendTurn("user", userText, pendingTurnLabel);
|
|
4751
4758
|
pendingTurnLabel = null;
|
|
4752
4759
|
if (currentAbort) {
|
|
@@ -4809,19 +4816,21 @@ function boot(cfg) {
|
|
|
4809
4816
|
collapseThinkingBlock();
|
|
4810
4817
|
},
|
|
4811
4818
|
onThinkingDelta: function(delta) {
|
|
4819
|
+
var following = atBottom();
|
|
4812
4820
|
var body = ensureThinkingBlock();
|
|
4813
4821
|
body.appendChild(document.createTextNode(delta));
|
|
4814
4822
|
body.scrollTop = body.scrollHeight;
|
|
4815
|
-
|
|
4823
|
+
if (following) pinToBottom();
|
|
4816
4824
|
},
|
|
4817
4825
|
onTextDelta: function(delta) {
|
|
4826
|
+
var following = atBottom();
|
|
4818
4827
|
markDone(assistantBody.roleLabel);
|
|
4819
4828
|
collapseThinkingBlock();
|
|
4820
4829
|
assistantMarkdown += delta;
|
|
4821
4830
|
renderMarkdownInto(assistantBody, assistantMarkdown);
|
|
4822
4831
|
liveTurn.content = assistantMarkdown;
|
|
4823
4832
|
persistSoon();
|
|
4824
|
-
|
|
4833
|
+
if (following) pinToBottom();
|
|
4825
4834
|
}
|
|
4826
4835
|
});
|
|
4827
4836
|
liveTurn.content = result.content;
|
|
@@ -937,23 +937,53 @@ export const PAGE_CARD_MEDIA_TYPE = "application/mcp-static-primitives+json"
|
|
|
937
937
|
// is already generous and a 1M-token model does not need 250 KB of it.
|
|
938
938
|
export const RESOURCE_SHARE_OF_WINDOW = 0.0625
|
|
939
939
|
|
|
940
|
+
// Bytes per token for SIZING that budget — not the estimator above, which
|
|
941
|
+
// measures real text. Kept at four so the budget stays exactly what it was
|
|
942
|
+
// (8 KB on a 32k model): how many tokens those bytes become depends on the
|
|
943
|
+
// resource, and re-sizing budgets is not this constant's job.
|
|
944
|
+
export const RESOURCE_BYTES_PER_TOKEN = 4
|
|
945
|
+
|
|
940
946
|
export function resourceBudgetBytes(window, { floor = 8000, cap = 64_000 } = {}) {
|
|
941
947
|
if (!window || !Number.isFinite(window)) return floor
|
|
942
|
-
const share = Math.round(window * RESOURCE_SHARE_OF_WINDOW *
|
|
948
|
+
const share = Math.round(window * RESOURCE_SHARE_OF_WINDOW * RESOURCE_BYTES_PER_TOKEN)
|
|
943
949
|
return Math.min(cap, Math.max(floor, share))
|
|
944
950
|
}
|
|
945
951
|
|
|
946
|
-
// A rough token count, the same
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
|
|
952
|
+
// A rough token count, the same three classes the hub's estimator uses.
|
|
953
|
+
//
|
|
954
|
+
// One ratio cannot describe both prose and a tool result. Letters and spaces
|
|
955
|
+
// tokenize at roughly 3.5 characters each; digits, punctuation and symbols go
|
|
956
|
+
// much denser, and a JSON tool result is mostly those.
|
|
957
|
+
//
|
|
958
|
+
// Measured against a real refusal (2026-10-17): a 289,101-character request
|
|
959
|
+
// the provider counted as 135,291 tokens was 33% letters, 16% digits and 45%
|
|
960
|
+
// punctuation. At a flat four characters per token that is 72,275 — 53% of the
|
|
961
|
+
// truth — so nothing was evicted and the provider refused the turn instead.
|
|
962
|
+
// These classes put it at 106%: high, which is the safe direction here, since
|
|
963
|
+
// underestimating means the turn dies while overestimating only gives up an
|
|
964
|
+
// old tool result slightly early.
|
|
965
|
+
export const PROSE_CHARS_PER_TOKEN = 3.5
|
|
966
|
+
export const DENSE_CHARS_PER_TOKEN = 1.6
|
|
967
|
+
export const CJK_CHARS_PER_TOKEN = 1
|
|
951
968
|
export const MESSAGE_OVERHEAD_TOKENS = 4
|
|
952
969
|
|
|
970
|
+
const CJK_PATTERN = /[\u3000-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]/g
|
|
971
|
+
const PROSE_PATTERN = /[A-Za-z \n\t]/g
|
|
972
|
+
|
|
973
|
+
export function estimateTextTokens(text) {
|
|
974
|
+
const str = typeof text === "string" ? text : String(text ?? "")
|
|
975
|
+
const cjk = (str.match(CJK_PATTERN) || []).length
|
|
976
|
+
const prose = (str.match(PROSE_PATTERN) || []).length
|
|
977
|
+
const dense = str.length - cjk - prose
|
|
978
|
+
return Math.ceil(cjk / CJK_CHARS_PER_TOKEN +
|
|
979
|
+
prose / PROSE_CHARS_PER_TOKEN +
|
|
980
|
+
dense / DENSE_CHARS_PER_TOKEN)
|
|
981
|
+
}
|
|
982
|
+
|
|
953
983
|
export function estimateTokens(messages) {
|
|
954
984
|
return (messages || []).reduce((sum, m) => {
|
|
955
985
|
const content = typeof m?.content === "string" ? m.content : JSON.stringify(m?.content ?? "")
|
|
956
|
-
return sum +
|
|
986
|
+
return sum + estimateTextTokens(content) + MESSAGE_OVERHEAD_TOKENS
|
|
957
987
|
}, 0)
|
|
958
988
|
}
|
|
959
989
|
|
|
@@ -969,7 +999,8 @@ export function estimateToolsTokens(tools) {
|
|
|
969
999
|
const text = (tools || []).map((t) => JSON.stringify({
|
|
970
1000
|
name: t?.name, description: t?.description, schema: t?.input_schema || t?.inputSchema
|
|
971
1001
|
})).join("")
|
|
972
|
-
|
|
1002
|
+
// Counted with the same classes: a schema is JSON, which is the dense case.
|
|
1003
|
+
return estimateTextTokens(text)
|
|
973
1004
|
}
|
|
974
1005
|
|
|
975
1006
|
// Makes room for the next round by dropping the OLDEST tool results, which is
|