From 3733ba80929c770cb04b57784693ae3b1710d1a1 Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:37:08 +0800 Subject: [PATCH] Stop estimate_tokens from double-counting text content estimate_tokens added message.all_text() and then serialized every content block (including text) with model_dump_json(), so text was counted twice and a pure-text message estimated at ~2x its real size. The docstring says it counts "text plus serialized non-text payloads"; skip content already covered by all_text() in the per-content loop so text is counted once, while non-text payloads are still serialized. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/fast_agent/history/compaction.py | 5 +++++ tests/unit/fast_agent/history/test_compaction.py | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/src/fast_agent/history/compaction.py b/src/fast_agent/history/compaction.py index 9ed91c9c9..4c06f5ca7 100644 --- a/src/fast_agent/history/compaction.py +++ b/src/fast_agent/history/compaction.py @@ -23,6 +23,7 @@ from fast_agent.constants import FAST_AGENT_COMPACTION_CHANNEL from fast_agent.core.logging.logger import get_logger from fast_agent.event_progress import ProgressAction +from fast_agent.mcp.helpers.content_helpers import get_text from fast_agent.mcp.prompt import Prompt from fast_agent.types.llm_stop_reason import LlmStopReason @@ -185,6 +186,10 @@ def estimate_tokens(messages: list[PromptMessageExtended]) -> int: for message in messages: chars += len(message.all_text()) for content in message.content: + if get_text(content) is not None: + # Text is already counted via all_text() above; only serialize + # non-text payloads here (matches the "non-text" docstring). + continue try: chars += len(content.model_dump_json()) except Exception: diff --git a/tests/unit/fast_agent/history/test_compaction.py b/tests/unit/fast_agent/history/test_compaction.py index 49de697c8..7514017ea 100644 --- a/tests/unit/fast_agent/history/test_compaction.py +++ b/tests/unit/fast_agent/history/test_compaction.py @@ -16,6 +16,7 @@ from fast_agent.config import CompactionSettings, Settings, get_settings from fast_agent.context import Context from fast_agent.history.compaction import ( + _CHARS_PER_TOKEN, DEFAULT_COMPACTION_PROMPT, FAST_AGENT_COMPACTION_CHANNEL, CompactionSkipped, @@ -359,6 +360,12 @@ def test_counts_non_text_content_and_channels(self): assert estimate_tokens([msg]) > plain + 5_000 + def test_does_not_double_count_text(self): + # Text is counted once via all_text(); the per-content loop must skip it + # (docstring: "Count text plus serialized non-text payloads"). + text = "x" * 1200 + assert estimate_tokens([_user(text)]) == len(text) // _CHARS_PER_TOKEN + class _FakeLLM: def __init__(self, summary: str = "SUMMARY OF WORK") -> None: