fix(model-context): count tool_calls in estimate_tokens so compaction sees real size (#2751)

This commit is contained in:
nubs
2026-06-05 13:56:54 +00:00
committed by GitHub
parent 8354948a1c
commit 6973c5427c
2 changed files with 68 additions and 1 deletions
+21 -1
View File
@@ -357,7 +357,11 @@ def estimate_tokens(messages: List[Dict]) -> int:
Uses chars * 0.3 which is closer to real BPE tokenizer output
than the commonly-cited chars/4 (which underestimates by ~20-30%).
Also adds ~4 tokens per message for role/formatting overhead.
Also adds ~4 tokens per message for role/formatting overhead, and counts
assistant tool_calls (name + arguments) — a tool-only turn carries
content=None with the real payload in tool_calls, so ignoring them made the
estimate (and the compaction/trim gates that rely on it) blind to large
tool arguments.
"""
total = 0
for msg in messages:
@@ -369,4 +373,20 @@ def estimate_tokens(messages: List[Dict]) -> int:
for item in content:
if isinstance(item, dict) and item.get("type") == "text":
total += int(len(item.get("text", "")) * 0.3)
# Tool calls carry real payload too: a tool-only assistant turn is stored
# with content=None and the actual args (e.g. a create_document body) in
# tool_calls[].function.arguments. Ignoring them made large tool arguments
# read as ~0 tokens, so the compaction/trim gates missed genuine overflow.
tool_calls = msg.get("tool_calls")
if isinstance(tool_calls, list):
for tc in tool_calls:
if not isinstance(tc, dict):
continue
fn = tc.get("function") if isinstance(tc.get("function"), dict) else tc
name = fn.get("name", "") or ""
args = fn.get("arguments", "") or ""
if not isinstance(args, str):
args = str(args) # some shapes store arguments as a dict
total += 4 # per tool-call overhead (id, type, wrapper)
total += int((len(str(name)) + len(args)) * 0.3)
return total