Skip to content

Add token count caching and performance instrumentation - #1692

Merged
aantn merged 5 commits into
masterfrom
claude/audit-token-counting-fZkU1
Mar 7, 2026
Merged

aantn merged 5 commits into
masterfrom
claude/audit-token-counting-fZkU1

Conversation

@aantn

@aantn aantn commented Mar 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR improves performance of token counting operations by implementing a caching mechanism for per-message token counts and adds comprehensive timing instrumentation to identify bottlenecks.

Key Changes

  • Token Count Caching: Modified count_tokens() in llm.py to reuse cached token_count values stored on individual messages, avoiding redundant tokenization calls. The cache is invalidated when messages are modified (e.g., during truncation).

  • Performance Instrumentation: Added timing measurements and debug logging to:

    • count_tokens(): Tracks elapsed time and reports cache hit/miss statistics
    • limit_input_context_window(): Measures total execution time and final token count
    • prevent_overly_big_tool_response(): Logs token counting duration per tool
    • truncate_messages_to_fit_context(): Measures sorting time for tool call messages
  • Optimized Token Lookups: Updated token counting calls in input_context_window_limiter.py to check for cached values before calling the expensive count_tokens_fn().

  • Token Count Updates in Streaming: Added token count metadata emission after tool results are processed in tool_calling_llm.py to provide updated context window information during streaming responses.

Implementation Details

  • Cache is stored as message["token_count"] and checked via message.get("token_count") before recounting
  • All timing uses time.monotonic() for accurate elapsed time measurement
  • Debug logging includes message counts, cache statistics, and token totals for observability
  • Changes are backward compatible - missing cached values gracefully fall back to recounting

https://claude.ai/code/session_01JHHMTC5p9gA1NunoyQb2Y8

Summary by CodeRabbit

  • Performance & Improvements

    • Message-level token caching to reduce redundant counting and speed up token calculations.
    • Streaming tool responses now include token-usage snapshots and cost reporting.
  • Observability & Privacy

    • Added timing and debug logging for token operations to improve monitoring.
    • Messages are sanitized before sending to providers to avoid exposing internal fields.

claude added 2 commits March 7, 2026 13:31
Reuse cached message["token_count"] in count_tokens() instead of
re-tokenizing every message on every call. Also use cached counts
in truncation sort and per-tool allocation to avoid redundant
litellm.token_counter calls. Added debug-level timing logs to all
major token counting call sites for performance visibility.

https://claude.ai/code/session_01JHHMTC5p9gA1NunoyQb2Y8
Signed-off-by: Claude <noreply@anthropic.com>
…ream

- Remove time.monotonic() timing instrumentation from count_tokens calls
- Remove unused `import time`
- Add token_count SSE event after tool results in call_stream, so streaming
  clients get updated token counts reflecting tool output (previously only
  emitted after LLM responses, not after tool execution)

https://claude.ai/code/session_01JHHMTC5p9gA1NunoyQb2Y8
Signed-off-by: Claude <noreply@anthropic.com>
@aantn
aantn enabled auto-merge (squash) March 7, 2026 17:56
@github-actions

github-actions Bot commented Mar 7, 2026 •

Copy link
Copy Markdown
Contributor

📂 Previous Runs

📜 Run @ f733759 (#22804511524)

✅ Results of HolmesGPT evals

Automatically triggered by commit f733759 on branch claude/audit-token-counting-fZkU1

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 9/9 test cases were successful, 0 regressions
Status Test case Time Turns Tools Cost Total tokens Input Output Cached Non-cached Reasoning Max output Compactions
✅ 09_crashpod 29.6s 5 11 $0.2438 107,954 106,109 1,845 80,663 25,446 — 596 —
✅ 101_loki_historical_logs_pod_deleted 45.3s 7 11 $0.2740 149,275 146,842 2,433 122,445 24,397 — 511 —
✅ 111_pod_names_contain_service 31.9s 5 12 $0.2391 105,879 103,864 2,015 79,964 23,900 — 505 —
✅ 112_find_pvcs_by_uuid 34.7s 7 9 $0.2644 148,980 147,071 1,909 122,015 25,056 — 432 —
✅ 12_job_crashing 32.2s 5 12 $0.2519 110,568 108,547 2,021 82,744 25,803 — 596 —
✅ 176_network_policy_blocking_traffic_no_runbooks 41.9s 7 17 $0.2974 157,261 154,920 2,341 126,381 28,539 — 423 —
✅ 24_misconfigured_pvc 28.2s 5 10 $0.2171 100,344 98,778 1,566 76,246 22,532 — 485 —
✅ 43_current_datetime_from_prompt 4.8s 1 — $0.1114 17,499 17,388 111 0 17,388 — 111 —
✅ 61_exact_match_counting 12.3s 3 2 $0.1488 56,217 55,859 358 36,385 19,474 — 171 —
Total 29.0s avg 5.0 avg 10.5 avg $2.0479 953,977 939,378 14,599 726,843 212,535 — 596 —
Benchmark Comparison Details

Baseline: latest ci-benchmark experiment on master

Status: Success - 61 test/model combinations loaded

Benchmark experiment:

Time comparison (seconds):

Test case This branch master Diff
09_crashpod (opus-4.5) 29.6s 29.5s ±0%
101_loki_historical_logs_pod_deleted (opus-4.5) 45.3s — —
111_pod_names_contain_service (opus-4.5) 31.9s 34.2s ±0%
112_find_pvcs_by_uuid (opus-4.5) 34.7s 31.0s ↑12%
12_job_crashing (opus-4.5) 32.2s 29.7s ±0%
176_network_policy_blocking_traffic_no_runbooks (opus-4.5) 41.9s 42.7s ±0%
24_misconfigured_pvc (opus-4.5) 28.2s 38.7s ↓27%
43_current_datetime_from_prompt (opus-4.5) 4.8s — —
61_exact_match_counting (opus-4.5) 12.3s — —

Benchmark has no cost, total tokens, cached tokens data. Will appear after the next weekly benchmark run.

Comparison indicators:

  • ±0% — diff under 10% (within noise threshold)
  • ↑N%/↓N% — diff 10-25%
  • ↑N%/↓N% — diff over 25% (significant)
📜 Run @ 196404f (#22804165959)

✅ Results of HolmesGPT evals

Automatically triggered by commit 196404f on branch claude/audit-token-counting-fZkU1

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 9/9 test cases were successful, 0 regressions
Status Test case Time Turns Tools Cost Total tokens Input Output Cached Non-cached Reasoning Max output Compactions
✅ 09_crashpod 27.9s 5 10 $0.2278 101,560 99,897 1,663 76,060 23,837 — 741 —
✅ 101_loki_historical_logs_pod_deleted 31.5s 4 8 $0.2250 85,012 83,042 1,970 59,697 23,345 — 761 —
✅ 111_pod_names_contain_service 42.6s 9 13 $0.2923 190,887 188,772 2,115 163,465 25,307 — 472 —
✅ 112_find_pvcs_by_uuid 29.0s 6 7 $0.2347 124,808 123,279 1,529 99,742 23,537 — 374 —
✅ 12_job_crashing 27.0s 5 11 $0.2391 108,075 106,534 1,541 80,433 26,101 — 436 —
✅ 176_network_policy_blocking_traffic_no_runbooks 49.4s 7 19 $0.3208 167,353 164,444 2,909 135,364 29,080 — 655 —
✅ 24_misconfigured_pvc 35.0s 6 14 $0.2528 124,434 122,415 2,019 97,571 24,844 — 705 —
✅ 43_current_datetime_from_prompt 4.9s 1 — $0.1115 17,503 17,388 115 0 17,388 — 115 —
✅ 61_exact_match_counting 12.2s 3 2 $0.1487 56,209 55,856 353 36,384 19,472 — 165 —
Total 28.8s avg 5.1 avg 10.5 avg $2.0527 975,841 961,627 14,214 748,716 212,911 — 761 —

✅ Results of HolmesGPT evals

Automatically triggered by commit c8d48f7 on branch claude/audit-token-counting-fZkU1

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 9/9 test cases were successful, 0 regressions
Status Test case Time Turns Tools Cost Total tokens Input Output Cached Non-cached Reasoning Max output Compactions
✅ 09_crashpod 26.3s 4 10 $0.2241 83,474 81,761 1,713 57,000 24,761 — 723 —
✅ 101_loki_historical_logs_pod_deleted 45.5s 7 11 $0.2778 149,091 146,626 2,465 121,532 25,094 — 438 —
✅ 111_pod_names_contain_service 31.5s 5 12 $0.2337 105,134 103,231 1,903 79,759 23,472 — 458 —
✅ 112_find_pvcs_by_uuid 29.5s 6 7 $0.2306 123,386 121,872 1,514 98,869 23,003 — 390 —
✅ 12_job_crashing 31.2s 5 12 $0.2502 109,188 107,206 1,982 81,326 25,880 — 487 —
✅ 176_network_policy_blocking_traffic_no_runbooks 43.2s 7 17 $0.2980 158,641 156,156 2,485 128,436 27,720 — 471 —
✅ 24_misconfigured_pvc 30.8s 5 13 $0.2414 105,359 103,445 1,914 78,378 25,067 — 692 —
✅ 43_current_datetime_from_prompt 5.2s 1 — $0.1131 17,564 17,388 176 0 17,388 — 176 —
✅ 61_exact_match_counting 14.2s 3 2 $0.1528 56,657 56,183 474 36,552 19,631 — 295 —
Total 28.6s avg 4.8 avg 10.5 avg $2.0217 908,494 893,868 14,626 681,852 212,016 — 723 —
Benchmark Comparison Details

Baseline: latest ci-benchmark experiment on master

Status: Success - 61 test/model combinations loaded

Benchmark experiment:

Time comparison (seconds):

Test case This branch master Diff
09_crashpod (opus-4.5) 26.3s 29.5s ↓11%
101_loki_historical_logs_pod_deleted (opus-4.5) 45.5s — —
111_pod_names_contain_service (opus-4.5) 31.5s 34.2s ±0%
112_find_pvcs_by_uuid (opus-4.5) 29.5s 31.0s ±0%
12_job_crashing (opus-4.5) 31.2s 29.7s ±0%
176_network_policy_blocking_traffic_no_runbooks (opus-4.5) 43.2s 42.7s ±0%
24_misconfigured_pvc (opus-4.5) 30.8s 38.7s ↓20%
43_current_datetime_from_prompt (opus-4.5) 5.2s — —
61_exact_match_counting (opus-4.5) 14.2s — —

Benchmark has no cost, total tokens, cached tokens data. Will appear after the next weekly benchmark run.

Comparison indicators:

  • ±0% — diff under 10% (within noise threshold)
  • ↑N%/↓N% — diff 10-25%
  • ↑N%/↓N% — diff over 25% (significant)
📖 Legend
Icon Meaning
✅ The test was successful
➖ The test was skipped
⚠️ The test failed but is known to be flaky or known to fail
🚧 The test had a setup failure (not a code regression)
🔧 The test failed due to mock data issues (not a code regression)
🚫 The test was throttled by API rate limits/overload
❌ The test failed and should be fixed before merging the PR
🔄 Re-run evals manually

⚠️ Warning: /eval comments always run using the workflow from master, not from this PR branch. If you modified the GitHub Action (e.g., added secrets or env vars), those changes won't take effect.

To test workflow changes, use the GitHub CLI or Actions UI instead:

gh workflow run eval-regression.yaml --repo HolmesGPT/holmesgpt --ref claude/audit-token-counting-fZkU1 -f markers=regression -f filter=

Option 1: Comment on this PR with /eval:

/eval
tags: regression

Or with more options (one per line):

/eval
model: gpt-4o
tags: regression
filter: 09_crashpod
iterations: 5

Run evals on a different branch (e.g., master) for comparison:

/eval
branch: master
tags: regression
Option Description
model Model(s) to test (default: same as automatic runs)
tags Pytest tags / markers (no default - runs all tests!)
filter Pytest -k filter (use /list to see valid eval names)
iterations Number of runs, max 10
branch Run evals on a different branch (for cross-branch comparison)

Quick re-run: Use /rerun to re-run the most recent /eval on this PR with the same parameters.

Option 2: Trigger via GitHub Actions UI → "Run workflow"

Option 3: Add PR labels to include extra evals in automatic regression runs:

Label Effect
evals-tag-<name> Run tests with tag <name> alongside regression
evals-id-<name> Run a specific eval by test ID

Examples: evals-tag-easy, evals-id-09_crashpod

🏷️ Valid tags

benchmark, chain-of-causation, compaction, confluence, context_window, coralogix, counting, database, datadog, datetime, db-connectors, easy, elasticsearch, embeds, fast, frontend, grafana-dashboard, hard, integration, kafka, kubernetes, leaked-information, logs, loki, medium, metrics, network, newrelic, no-cicd, numerical, one-test, port-forward, prometheus, question-answer, regression, runbooks, slackbot, storage, toolset-limitation, traces, transparency


Commands: /eval · /rerun · /list

CLI: gh workflow run eval-regression.yaml --repo HolmesGPT/holmesgpt --ref claude/audit-token-counting-fZkU1 -f markers=regression -f filter=

@github-actions

github-actions Bot commented Mar 7, 2026 •

Copy link
Copy Markdown
Contributor

✅ Docker images ready for b4795d0a (built in 1m 17s)

⚠️ Warning: does not support ARM (ARM images are built on release only - not on every PR)

Use these tags to pull the images for testing.

📋 Copy commands

⚠️ Temporary images are deleted after 30 days. Copy to a permanent registry before using them:

gcloud auth configure-docker us-central1-docker.pkg.dev
docker pull us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes:b4795d0a
docker tag us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes:b4795d0a me-west1-docker.pkg.dev/robusta-development/development/holmes-dev:b4795d0a
docker push me-west1-docker.pkg.dev/robusta-development/development/holmes-dev:b4795d0a
docker pull us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes-operator:b4795d0a
docker tag us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes-operator:b4795d0a me-west1-docker.pkg.dev/robusta-development/development/holmes-operator-dev:b4795d0a
docker push me-west1-docker.pkg.dev/robusta-development/development/holmes-operator-dev:b4795d0a

Patch Helm values in one line (choose the chart you use):

HolmesGPT chart:

helm upgrade --install holmesgpt ./helm/holmes \
  --set registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set image=holmes-dev:b4795d0a \
  --set operator.registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set operator.image=holmes-operator-dev:b4795d0a

Robusta wrapper chart:

helm upgrade --install robusta robusta/robusta \
  --reuse-values \
  --set holmes.registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set holmes.image=holmes-dev:b4795d0a \
  --set holmes.operator.registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set holmes.operator.image=holmes-operator-dev:b4795d0a

@coderabbitai

coderabbitai Bot commented Mar 7, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c32b5c85-7878-4a11-935f-6a7a9c3289f8

📥 Commits

Reviewing files that changed from the base of the PR and between 196404f and c8d48f7.

📒 Files selected for processing (1)
  • holmes/core/llm.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • holmes/core/llm.py

Walkthrough

Adds per-message token-count caching and time-based instrumentation to token counting, sanitizes messages before provider calls, and emits token-count/cost snapshots during tool-call streaming. Several modules receive monotonic-timing logs around counting and truncation operations.

Changes

Cohort / File(s) Summary
Token counting core
holmes/core/llm.py
Adds per-message token_count caching, records elapsed time for count_tokens, logs cached vs counted message counts, and sanitizes messages (removes internal token_count) before provider calls.
Streaming + tooling
holmes/core/tool_calling_llm.py
After tool result handling in call_stream, computes current token totals/costs and yields token-count stream events so streaming includes token-usage snapshots.
Context window & truncation instrumentation
holmes/core/tools_utils/tool_context_window_limiter.py, holmes/core/truncation/input_context_window_limiter.py
Adds time.monotonic() timing around token-count operations and sorting, uses defensive fallbacks to recompute tokens when token_count missing, and logs durations and truncation actions.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant Streamer as tool_calling_llm.call_stream
    participant Tools
    participant LLM as holmes/core/llm.count_tokens
    participant Provider

    Client->>Streamer: open stream / send messages
    Streamer->>Tools: invoke tool(s)
    Tools-->>Streamer: tool results
    Streamer->>LLM: count_tokens(current messages + tools)
    LLM-->>Streamer: token totals (may use cached token_count)
    Streamer->>Provider: send sanitized messages (no token_count)
    Provider-->>Streamer: provider responses (streamed)
    Streamer-->>Client: yield tool result + token-count snapshot (usage/cost metadata)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • nherment
  • moshemorad
  • arikalon1
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes in the pull request: implementing token count caching and adding performance instrumentation across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@netlify

netlify Bot commented Mar 7, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for holmes-docs ready!

Name Link
🔨 Latest commit c8d48f7
🔍 Latest deploy log https://app.netlify.com/projects/holmes-docs/deploys/69ac6c84d061d50008cf6c30
😎 Deploy Preview https://deploy-preview-1692--holmes-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions

github-actions Bot commented Mar 7, 2026 •

Copy link
Copy Markdown
Contributor

🔬 CLI Performance Benchmark

🟡 Startup Time (no LLM)

Measures holmes version execution time (imports + initialization)

Metric PR Master Change
Cold Start 11.17s 11.15s +0.1%
Warm Mean 4.96s 5.13s -3.3%
Warm Min 4.94s 5.09s
Warm Max 4.97s 5.18s

🟡 Full CLI with LLM

Measures holmes ask execution time (OpenRouter + Haiku 4.5)

Metric PR Master Change
Cold Start 25.89s 30.97s -16.4%
Warm Mean 7.11s 7.27s -2.2%
Warm Min 7.01s 7.21s
Warm Max 7.25s 7.37s

PR: b4795d0a | Master: 8446ca6f | Iterations: 5

arikalon1
arikalon1 previously approved these changes Mar 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
holmes/core/truncation/input_context_window_limiter.py (1)

104-122: ⚠️ Potential issue | 🟠 Major

Count uncached tool messages from the full payload.

When the cache is missing, this fallback only tokenizes {"role": "tool", "content": ...}. Actual tool messages here also include name and tool_call_id, so needed_space can be underestimated and truncation can still leave the prompt over budget. Using or here also treats a cached 0 as a miss.

Suggested fix
+    def get_tool_message_token_count(message: dict[str, Any]) -> int:
+        cached_token_count = message.get("token_count")
+        if cached_token_count is not None:
+            return cached_token_count
+
+        token_count = count_tokens_fn([message]).total_tokens
+        message["token_count"] = token_count
+        return token_count
+
     t_sort = time.monotonic()
-    tool_call_messages.sort(
-        key=lambda x: x.get("token_count") or count_tokens_fn(
-            [{"role": "tool", "content": x["content"]}]
-        ).total_tokens
-    )
+    tool_call_messages.sort(key=get_tool_message_token_count)
@@
-        needed_space = msg.get("token_count") or count_tokens_fn(
-            [{"role": "tool", "content": msg["content"]}]
-        ).total_tokens
+        needed_space = get_tool_message_token_count(msg)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/core/truncation/input_context_window_limiter.py` around lines 104 -
122, The count fallback currently underestimates tool tokens and treats cached
zero as missing; update both the sort key lambda and the needed_space
calculation to use the cached token_count only when it is not None (e.g., "if
msg.get('token_count') is not None") and otherwise call count_tokens_fn with the
full tool message fields (include "name" and "tool_call_id" along with "role"
and "content") so the tokenizer sees the same shape as the real payload; adjust
references around tool_call_messages, count_tokens_fn, and needed_space
accordingly.
🧹 Nitpick comments (1)
holmes/core/tools_utils/tool_context_window_limiter.py (1)

37-41: Rename t0 to a descriptive timer variable.

The instrumentation is fine, but t0 obscures what is being timed. A name like count_tokens_started_at keeps the log path self-documenting.

As per coding guidelines, **/*.{js,ts,tsx,jsx,py,java,cs,go,rb,php}: Use semantic, descriptive names for variables, functions, and components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/core/tools_utils/tool_context_window_limiter.py` around lines 37 - 41,
Rename the ambiguous timer variable t0 to a descriptive name like
count_tokens_started_at in the block that measures token counting (surrounding
message = tool_call_result.as_tool_call_message(), messages_token =
llm.count_tokens(...), and the logging.debug call inside
prevent_overly_big_tool_response), and update the logging.debug timestamp
expression to use the new variable (count_tokens_started_at) so the timing
measurement remains correct and self-documenting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@holmes/core/llm.py`:
- Around line 314-323: count_tokens() currently mutates message dicts by setting
message["token_count"] (seen in count_tokens / the shown block) which then can
be forwarded to providers; before calling completion() (or inside
parse_messages_tags()), strip any non-schema fields by removing "token_count"
from each message (e.g., msg.pop("token_count", None)) so provider chat
completion APIs only receive valid schema fields—implement this sanitization
either at the end of parse_messages_tags() or immediately prior to the
completion() call in the completion pipeline (keeping the existing truncation
behavior in input_context_window_limiter.py for reference).

In `@holmes/core/tool_calling_llm.py`:
- Around line 1204-1214: The token-count snapshot emitted by
build_stream_event_token_count is taken too early and can be stale because
messages and tools are still mutated later (e.g. appending pending_approval to
messages and replacing tools on runbook activation); change the sequence so that
self.llm.count_tokens(...) and add_token_count_to_metadata(...) are called only
after all remaining state changes (including the pending_approval append and any
tools replacement after runbook activation) have been applied to messages and
tools, then update metadata["costs"] and yield
build_stream_event_token_count(metadata=metadata) using the final messages/tools
used in the actual response; keep references to the existing helpers
(self.llm.count_tokens, add_token_count_to_metadata,
build_stream_event_token_count, metadata, messages, tools, pending_approval,
limit_result, full_response) when moving the logic.

---

Outside diff comments:
In `@holmes/core/truncation/input_context_window_limiter.py`:
- Around line 104-122: The count fallback currently underestimates tool tokens
and treats cached zero as missing; update both the sort key lambda and the
needed_space calculation to use the cached token_count only when it is not None
(e.g., "if msg.get('token_count') is not None") and otherwise call
count_tokens_fn with the full tool message fields (include "name" and
"tool_call_id" along with "role" and "content") so the tokenizer sees the same
shape as the real payload; adjust references around tool_call_messages,
count_tokens_fn, and needed_space accordingly.

---

Nitpick comments:
In `@holmes/core/tools_utils/tool_context_window_limiter.py`:
- Around line 37-41: Rename the ambiguous timer variable t0 to a descriptive
name like count_tokens_started_at in the block that measures token counting
(surrounding message = tool_call_result.as_tool_call_message(), messages_token =
llm.count_tokens(...), and the logging.debug call inside
prevent_overly_big_tool_response), and update the logging.debug timestamp
expression to use the new variable (count_tokens_started_at) so the timing
measurement remains correct and self-documenting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 47d16961-bb4e-4563-bc9c-b62676683ce0

📥 Commits

Reviewing files that changed from the base of the PR and between 86c740c and 196404f.

📒 Files selected for processing (4)
  • holmes/core/llm.py
  • holmes/core/tool_calling_llm.py
  • holmes/core/tools_utils/tool_context_window_limiter.py
  • holmes/core/truncation/input_context_window_limiter.py

Comment thread holmes/core/llm.py
Comment thread holmes/core/tool_calling_llm.py
aantn and others added 3 commits March 7, 2026 20:17
…iders

count_tokens() caches per-message token counts by setting message["token_count"]
on the dict objects. These same dicts are passed to litellm.completion() and
ultimately to provider APIs (OpenAI, Anthropic, etc.) which may reject unknown
fields. Sanitize in DefaultLLM.completion() — the single gateway to litellm —
so all callers are covered (including compaction which bypasses
parse_messages_tags). Shallow-copies only dicts that have the field to avoid
invalidating the cache.

Finding 2 (token count snapshot ordering in streaming) was verified as not a
real issue: subsequent mutations are either negligible (pending_approval flag)
or captured in the next iteration (runbook tool replacement).

https://claude.ai/code/session_01JHHMTC5p9gA1NunoyQb2Y8
Signed-off-by: Claude <noreply@anthropic.com>
@aantn
aantn merged commit 7ef3f5f into master Mar 7, 2026
21 of 22 checks passed
@aantn
aantn deleted the claude/audit-token-counting-fZkU1 branch March 7, 2026 19:11
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.

3 participants