Skip to content

Count assistant tool_calls in count_message_tokens - #232

Merged
t-timms merged 2 commits into
mainfrom
fix/token-count-tool-calls
Sep 24, 2026
Merged

t-timms merged 2 commits into
mainfrom
fix/token-count-tool-calls

Conversation

@t-timms

@t-timms t-timms commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Why

count_message_tokens drives context compaction. It encoded only string values on a message or one level inside list items, so the nested function dict of assistant tool_calls (tool name and arguments) was never counted.

Observed on a local llama-server with a hard 32K window (KAT-Coder REAP-50, Godspeed agent-in-loop runner, SWE-bench Lite dev): Godspeed logged compaction at tokens=26999 / 27944 / 34473 (its own estimate), yet the server hit n_tokens = 32767, truncated = 1 five times in one run. Each time the reply was cut off in the middle of a tool call, llama-server answered HTTP 500 Failed to parse tool call arguments as JSON ... missing closing quote on every retry, and the session ended with exit_reason=llm_error. Those sessions emitted 80-170 tool calls.

Changes

  • count_message_tokens now recurses through lists and dicts (_collect_strings), so tool-call names and arguments count. image_url blocks are still counted flat and not encoded.
  • Docstring states what the estimate does not see: the chat template's own markup and the tool schemas sent with the messages.
  • tests/test_token_counter.py: tool-call arguments counted, tool-call name counted, plain content unchanged, content blocks counted, image blocks flat-estimated without encoding their URL. The two tool-call tests fail without the change.

Measured effect (real requests)

18 requests sampled from a Qwen3.8-27B agent-in-loop run (captured with a logging proxy, 8 SWE-bench tasks), true tokens = server /apply-template (with tools) then /tokenize:

estimator true / estimate, all requests true / estimate, prompts > 8K tokens
main (before) median 1.29x, max 1.65x median 1.27x, max 1.29x
this PR median 1.11x, max 1.65x median 1.08x, max 1.12x

So on main the estimate is 22-39% too low, and the default compaction threshold of 0.8 on a 32K window corresponds to roughly 0.8 x 1.29 x 32K = 33.8K true tokens, i.e. already past the window: compaction cannot prevent the overflow. With this PR the same threshold corresponds to about 29K true tokens. Tool schemas add a further median of 562 tokens in this runner (more in the full harness) and are still not estimated.

Verification

  • 523 passed, 1 skipped across test_token_counter, test_multimodal, test_agent_loop, test_context/. ruff check . and ruff format --check . (CI flags) clean.

Not verified / limits

  • The remaining ~8-11% gap is the tokenizer (cl100k versus the model's own) and template markup. The 29K-true-tokens headroom below a 32K window is thin for a turn that returns several large tool outputs at once (KAT emits ~4-5 parallel calls per turn), so a bigger safety margin or calibrating against the server's reported usage.prompt_tokens would be a sensible follow-up; that is not part of this PR.
  • The one max-ratio request (1.65x) is a small early-conversation prompt where fixed template overhead dominates.

count_message_tokens only encoded string values found directly on a message or
one level down inside list items. Assistant tool_calls look like
{"function": {"name": ..., "arguments": "..."}}, so the nested `function`
dict was skipped and tool-call names and arguments were never counted. In a
tool-heavy session (many long shell commands) those arguments can be most of
the conversation, so the estimate that drives compaction ran well under the
real size.

Recurse through lists and dicts instead (image_url blocks are still counted
flat and not encoded). The docstring now says what the estimate does not see:
the chat template's own markup and the tool schemas sent alongside the messages.

Tests: tests/test_token_counter.py. The two tool-call tests fail without this
change.
@github-actions

Copy link
Copy Markdown

🤖 Godspeed Review

LLM review skipped (no API key configured).

Security

No secrets detected in changed files.

@github-actions

Copy link
Copy Markdown

🤖 Godspeed Review

LLM review skipped (no API key configured).

Security

No secrets detected in changed files.

@t-timms
t-timms merged commit 02b341d into main Sep 24, 2026
9 checks passed
@t-timms
t-timms deleted the fix/token-count-tool-calls branch September 24, 2026 20:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant