Skip to content

fix/over-counts tool outputs and blocks legitimate conversations#3316

Closed
ezkemboi wants to merge 12 commits into
dimagi:mainfrom
ezkemboi:fix-3303/message-size-validations
Closed

fix/over-counts tool outputs and blocks legitimate conversations#3316
ezkemboi wants to merge 12 commits into
dimagi:mainfrom
ezkemboi:fix-3303/message-size-validations

Conversation

@ezkemboi

@ezkemboi ezkemboi commented May 11, 2026

Copy link
Copy Markdown
Contributor

Product Description

Users sending short messages were sometimes seeing errors like:

"Your message is too large for this model."

…even though their input was only a few words. This happened when a tool called during the conversation returned a large response (e.g. a Custom Action returning structured JSON). This fix resolves those false positives and re-enables the middleware that was temporarily disabled in #3302.

The frontend character counter (added in #3180) has also been removed. It was built on the same flawed approximation as the middleware and independently produced false positives — blocking the send button for messages the model would have accepted. The server-side error message now surfaces directly in the chat when a message is genuinely too large.

Technical Description

MessageSizeValidationMiddleware.before_model previously ran count_tokens_approximately(state["messages"]) — counting all messages in state on every agent loop iteration, including ToolMessage content from tool calls.

Three problems stacked:

  1. ToolMessage inflation — on iteration 2 (after a tool call returns), the full state including the large ToolMessage was counted, blowing past the token limit even though the user's input was small.
  2. System prompt double-countingeffective_limit already deducts system tokens from the budget, but state["messages"] can include the SystemMessage, so those tokens were subtracted twice.
  3. chars/4 approximation overshootcount_tokens_approximately uses chars / 4, which overshoots JSON and structured content by 1.3–2× under real BPE. Even when the count was "too large", the model may have accepted the request.

Fix: filter state["messages"] to only the last HumanMessage before counting, and switch to model.get_num_tokens_from_messages (real BPE tokenizer via tiktoken for OpenAI models).

Key changes:

  • history_middleware.pybefore_model counts only human_messages[-1:] using model.get_num_tokens_from_messages; token_limit=None skips validation, token_limit=0 (system prompt exhausted full budget) correctly rejects
  • llm_node.py — middleware re-enabled (was commented out in fix(pipelines): temporarily disable MessageSizeValidationMiddleware #3302); model = node.get_chat_model() extracted once and passed to both the middleware and create_agent to avoid double instantiation; system token count also uses real tokenizer
  • test_llm_node.py — regression tests for ToolMessage false positive, empty HumanMessage state, multi-turn history, zero budget, and None budget
  • utils.py, views/utils.py, experiment.py, chatbots/views.py, pipelines/views.pycompute_max_char_limit / get_max_char_limit and all max_char_limit template context keys removed
  • input_bar.htmlmaxCharLimit, messageTooLong, :maxlength, error class bindings removed
  • apps/pipelines/utils.py — deleted (emptied by counter removal)
  • test_view_utils.py — deleted (all tests were for the removed get_max_char_limit)

Migrations

  • The migrations are backwards compatible

No migrations.

Demo

No visible UI change on the happy path. Users who were previously blocked by false positives will now be able to send messages normally. When a message genuinely exceeds the model's token budget, a user-facing error appears inline in the chat.

Docs and Changelog

  • This PR requires docs/changelog update

codescene-delta-analysis[bot]

This comment was marked as outdated.

@ezkemboi

Copy link
Copy Markdown
Contributor Author

@snopoke, I am doing more testing on this PR, later today and tomorrow, for it to be ready.
I will also let you know of the JSON and structured tool output tokenize because that has not been added here yet.

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR updates the message size validation middleware to focus token-budget enforcement on only the most recent HumanMessage, excluding tool messages and prior conversation history. The MessageSizeValidationMiddleware docstring and validation logic in before_model are rewritten to filter messages by type, skip validation when no HumanMessage exists, and evaluate only the last human message against the token limit. The middleware is re-enabled in build_node_agent, removing the prior disabled comment. Three new tests verify that oversized tool messages do not raise exceptions, validation is skipped when no HumanMessage exists, and only the most recent HumanMessage is checked.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: fixing token counting that was blocking legitimate conversations by incorrectly including tool outputs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering product impact, technical root cause, implementation details, and test coverage with clear before/after context.

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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/pipelines/nodes/history_middleware.py`:
- Around line 148-153: The current early-exit uses "if not self._token_limit"
which treats 0 as "no limit" and skips validation; change that to only skip when
the limit is unknown (e.g., "if self._token_limit is None") so a token_limit of
0 is handled as a real (rejecting) limit, then ensure the validation logic
compares token usage against self._token_limit (using <= or < as appropriate)
and rejects when the limit is exceeded or equals zero; look for _token_limit and
_build_size_validation_middleware to update the guard, and ensure
count_tokens_approximately and HumanMessage handling remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4cf33224-306d-4be3-be6b-a72b0267757e

📥 Commits

Reviewing files that changed from the base of the PR and between aa80d94 and 925f32f.

📒 Files selected for processing (3)
  • apps/pipelines/nodes/history_middleware.py
  • apps/pipelines/nodes/llm_node.py
  • apps/pipelines/tests/test_llm_node.py

Comment thread apps/pipelines/nodes/history_middleware.py Outdated
codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@ezkemboi

Copy link
Copy Markdown
Contributor Author

From the comment #3196 (comment), I have also removed counter on this end.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@ezkemboi ezkemboi marked this pull request as ready for review May 17, 2026 05:21
codescene-delta-analysis[bot]

This comment was marked as outdated.

@ezkemboi

ezkemboi commented May 17, 2026

Copy link
Copy Markdown
Contributor Author

@snopoke and @SmittieC, I’ve removed the frontend counter from this test.

Also, on the current change, we only value the last message and not with history.

The backend now uses _count_tokens, which relies on get_num_tokens_from_messages for accurate token counting, including proper handling of JSON-structured inputs.

For models that don’t support get_num_tokens_from_messages, we fall back to count_tokens_approximately (langchain-based estimation).

I have attached a screenshot of the test done.
Screenshot 2026-05-17 at 08 27 41

cc. @KipmurkorDev, please help retest the changes updated.

@ezkemboi

Copy link
Copy Markdown
Contributor Author

@SmittieC, if you get some time today, help review this PR for me.

cc. @snopoke

@SmittieC

Copy link
Copy Markdown
Contributor

@SmittieC, if you get some time today, help review this PR for me.

cc. @snopoke

Sure I'll take a look today

Comment thread apps/pipelines/nodes/mixins.py Outdated

# Reserve space for the system message so trigger/keep thresholds reflect usable context
system_message_tokens = count_tokens_approximately([system_message])
from apps.pipelines.nodes.history_middleware import _count_tokens # noqa: PLC0415

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.

can this import be moved to the top?

Comment thread apps/pipelines/nodes/llm_node.py Outdated
if not max_token_limit:
return None
system_tokens = count_tokens_approximately([system_message])
system_tokens = _count_tokens(model, [system_message])

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.

Rather make _count_tokens non-private if we're going to be importing it from another module

Comment thread apps/pipelines/nodes/mixins.py Outdated
system_message_tokens = count_tokens_approximately([system_message])
from apps.pipelines.nodes.history_middleware import _count_tokens # noqa: PLC0415

system_message_tokens = _count_tokens(model, [system_message]) if model is not None else 0

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.

Why assume 0 tokens if the model is missing? _count_tokens will fall back to approximating it if the model is missing, which seems like something we'd want?

from apps.pipelines.exceptions import MessageTooLargeError


def _count_tokens(model, messages: list) -> int:

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.

Suggestion:

You're passing the model from the node level down to here just to call .get_num_tokens_from_messages on it. What if you instead make a method that accepts an optional model param and return the token counter to use. Something like this:

def get_token_counter(model=None):
    return model.get_num_tokens_from_messages if model else count_tokens_approximately

That way, the history middleware (and _build_size_validation_middleware) doesn't have to get a model from its callers, but just the token counter to use.

Regarding the try-catch behaviour: What scenarios might there be where model.get_num_tokens_from_messages raises and count_tokens_approximately doesn't? Maybe the caller should be responsible for doing any try-catching?

f"Your message is too large for this model. "
f"It uses approximately {token_count} tokens, but only {self._token_limit} tokens are available."
f"It uses approximately {token_count} tokens, but only {self._token_limit} tokens are available "
f"after accounting for the system prompt."

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.

after accounting for the system prompt

Nit: Just double checking, is there a specific reason why we need to include this?

if not human_messages:
return None
token_count = count_tokens_approximately(state["messages"])
token_count = _count_tokens(self._model, [human_messages[-1]])

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.

Do you know if the history middleware will remove all history when the human message is long enough that it effectively takes up all of the token space that is left over after subtracting the system message's tokens?

I have a feeling that a large human message might still fail at the API level, even though it passes here, unless the history middleware is smart enough to throw everything away except the last human and system message. Do you perhaps know if this is the case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Previously, the validator only checked the last human message against the token limit. The compressors (summarize/truncate) handled reducing history, but if they couldn't e.g. SUMMARIZE with fewer than 20 messages, or MAX_HISTORY_LENGTH which is count-based, a large message + existing history could slip through and fail silently at the API. That's exactly the edge case you caught.

The changes add a second check on the total conversation history (HumanMessages + AIMessages) that runs after the compressor. If the compressor couldn't reduce context enough, this raises a clear error before the API is called. ToolMessages are excluded because they sit between agent-loop iterations and aren't user-controlled, that was actually the root cause of #3303 where the previous total-context check was firing incorrectly.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@ezkemboi

Copy link
Copy Markdown
Contributor Author

@SmittieC, we are checking on the feedback and retesting based on the knowledge shared with @KipmurkorDev

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

f"It uses approximately {token_count} tokens, but only {self._token_limit} tokens are available."
)

# Validate total history, excluding tool responses and the system prompt.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@SmittieC you can check on this approach and let me know what are your thoughts?

and history size is managed by the compression middleware that runs before this one.
Two checks run on each call: the last HumanMessage is validated individually, and
the total conversation history (HumanMessages + AIMessages) is validated together.
ToolMessages and SystemMessages are excluded from the total — tool responses are

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.

I don't quite follow why tool responses are excluded. They are also sent to the LLM along with the history + user's new message, so it should be included, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I will include that as well in this PR.

Previously, I thought we were only validating the latest user message against the configured max_token_limit. Since some models support hundreds of thousands of tokens, whereas in our case we may configure a lower limit, such as 8192, I assumed the validation was only applied to the latest message and not to the entire payload.

From your explanation, I now understand that the validation should apply to the full request sent to the LLM, including the conversation history and tool responses, since all of that contributes to actual token usage.

middleware.append(history_middleware)
if size_middleware := _build_size_validation_middleware(node, system_message, model):
middleware.append(size_middleware)
middleware = node.build_history_middleware(system_message=system_message, model=model)

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.

Suggestion: Keep the history middleware as it was before and always include MessageSizeValidationMiddleware in the middleware, since there will always be a max token limit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Well noted. I will do so.

@snopoke snopoke 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.

@ezkemboi @SmittieC I'm wondering about the utility of this feature. The issue it is trying to resolve is quite rare and requires the user to upload a large document and paste a very large amount of text.

Given the complexity that validating this is turning out to be I'd like to propose we drop it entirely.

@ezkemboi

Copy link
Copy Markdown
Contributor Author

@ezkemboi @SmittieC I'm wondering about the utility of this feature. The issue it is trying to resolve is quite rare and requires the user to upload a large document and paste a very large amount of text.

Given the complexity that validating this is turning out to be I'd like to propose we drop it entirely.

@snopoke, since @SmittieC was the creator of the same issue, his feedback carries more weight on the proposal above.

@SmittieC

Copy link
Copy Markdown
Contributor

@ezkemboi @SmittieC I'm wondering about the utility of this feature. The issue it is trying to resolve is quite rare and requires the user to upload a large document and paste a very large amount of text.

Given the complexity that validating this is turning out to be I'd like to propose we drop it entirely.

I was starting to think along the same lines. The only benefit we get by checking and failing early is user feedback. If we still want the user feedback - which I do think is nice, because it tells users what they can do about it - we might as well let the API call fail, parse the failure and reraise a custom user friendly error.

@ezkemboi

Copy link
Copy Markdown
Contributor Author

@ezkemboi @SmittieC I'm wondering about the utility of this feature. The issue it is trying to resolve is quite rare and requires the user to upload a large document and paste a very large amount of text.
Given the complexity that validating this is turning out to be I'd like to propose we drop it entirely.

I was starting to think along the same lines. The only benefit we get by checking and failing early is user feedback. If we still want the user feedback - which I do think is nice, because it tells users what they can do about it - we might as well let the API call fail, parse the failure and reraise a custom user friendly error.

I think what you have mentioned above makes sense.
Let's hear what @snopoke has to say regarding the same.

@snopoke

snopoke commented May 25, 2026

Copy link
Copy Markdown
Contributor

+1 to parsing the error - Here is the error you get from OpenAI:

 {'error': {'message': 'Your input exceeds the context window of this model. Please adjust your input and try again.', 'type': 'invalid_request_error', 'param': 'input', 'code': 'context_length_exceeded'}}

I'm not sure what you get from the other providers.

@ezkemboi

Copy link
Copy Markdown
Contributor Author

+1 to parsing the error - Here is the error you get from OpenAI:

 {'error': {'message': 'Your input exceeds the context window of this model. Please adjust your input and try again.', 'type': 'invalid_request_error', 'param': 'input', 'code': 'context_length_exceeded'}}

I'm not sure what you get from the other providers.

I will take a look at this PR this weekend so that we can get it out of line.

@ezkemboi

Copy link
Copy Markdown
Contributor Author

+1 to parsing the error - Here is the error you get from OpenAI:

 {'error': {'message': 'Your input exceeds the context window of this model. Please adjust your input and try again.', 'type': 'invalid_request_error', 'param': 'input', 'code': 'context_length_exceeded'}}

I'm not sure what you get from the other providers.

for copilot model of Gpt is also the same as mentioned

Reason: Request Failed: 400 {"error":{"message":"This model's maximum context length is 128000 tokens. However, your messages resulted in 132438 tokens (108594 in the messages, 23844 in the functions). Please reduce the length of the messages or functions.","code":"invalid_request_body"}}

@snopoke snopoke closed this Jun 30, 2026
@ezkemboi

Copy link
Copy Markdown
Contributor Author

@snopoke noted.
Will check on another issue and work on it.

@ezkemboi ezkemboi deleted the fix-3303/message-size-validations branch June 30, 2026 10:45
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