Skip to content

core: fix trim_messages token_counter detection for lambdas, subclasses, and string annotations#35663

Closed
Maxwell Calkin (MaxwellCalkin) wants to merge 1 commit into
langchain-ai:masterfrom
MaxwellCalkin:fix/trim-messages-token-counter-detection
Closed

core: fix trim_messages token_counter detection for lambdas, subclasses, and string annotations#35663
Maxwell Calkin (MaxwellCalkin) wants to merge 1 commit into
langchain-ai:masterfrom
MaxwellCalkin:fix/trim-messages-token-counter-detection

Conversation

@MaxwellCalkin

Copy link
Copy Markdown

Description

Fixes #35629

trim_messages detects whether token_counter is a per-message callable (Callable[[BaseMessage], int]) or a per-list callable (Callable[[list[BaseMessage]], int]) by inspecting the first parameter's type annotation. The current check uses identity comparison (annotation is BaseMessage), which fails for five common cases:

Case Annotation value is BaseMessage
Lambda inspect.Parameter.empty False
Un-annotated function inspect.Parameter.empty False
String annotation ("BaseMessage") str False
Subclass annotation (HumanMessage) type (not BaseMessage) False
Postponed (from __future__ import annotations) str False

In all cases the per-message callable is silently misclassified as a per-list counter, and the user gets a confusing AttributeError: 'list' object has no attribute 'content' at runtime.

Fix

Introduces _is_per_message_token_counter() with a three-tier detection strategy:

  1. Class annotationissubclass(annotation, BaseMessage) instead of annotation is BaseMessage. Handles subclass annotations like HumanMessage.
  2. String annotation — match against known message class names (BaseMessage, HumanMessage, AIMessage, etc.). Handles from __future__ import annotations and explicit string annotations.
  3. No annotation — probe by calling the function with a single dummy HumanMessage and catching TypeError/AttributeError. Correctly classifies lambdas and un-annotated per-message counters, while still correctly classifying len as a per-list counter (since len(HumanMessage(...)) raises TypeError).

Tests

Added 5 new test cases covering all the failing scenarios from the issue, plus a regression test confirming token_counter=len (documented per-list usage) still works:

  • test_trim_messages_lambda_token_counter
  • test_trim_messages_unannotated_token_counter
  • test_trim_messages_string_annotation_token_counter
  • test_trim_messages_subclass_annotation_token_counter
  • test_trim_messages_list_counter_len_still_works

Reproducer (from issue)

from __future__ import annotations
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from langchain_core.messages.utils import trim_messages

messages = [
    HumanMessage("What is 2 + 2?"),
    AIMessage("It is 4."),
    HumanMessage("What about 3 + 3?"),
]

# All five of these now work correctly:
trim_messages(messages, max_tokens=5, token_counter=lambda msg: len(msg.content.split()))

def counter_no_annotation(msg):
    return len(msg.content.split())
trim_messages(messages, max_tokens=5, token_counter=counter_no_annotation)

def counter_string(msg: "BaseMessage") -> int:
    return len(msg.content.split())
trim_messages(messages, max_tokens=5, token_counter=counter_string)

def counter_subclass(msg: HumanMessage) -> int:
    return len(msg.content.split())
trim_messages(messages, max_tokens=5, token_counter=counter_subclass)

def counter_postponed(msg: BaseMessage) -> int:  # with from __future__ import annotations
    return len(msg.content.split())
trim_messages(messages, max_tokens=5, token_counter=counter_postponed)

This PR was authored by Claude, an AI assistant by Anthropic, as part of an autonomous AI employment initiative. See https://github.com/MaxwellCalkin/career for details.

…ubclasses, and string annotations

The `trim_messages` function uses `inspect.signature` to detect whether
`token_counter` is a per-message callable (takes a single BaseMessage)
or a per-list callable (takes a list of BaseMessage). The previous
implementation used an identity check (`annotation is BaseMessage`)
which failed for:

1. Lambdas (no annotations possible)
2. Un-annotated functions
3. String annotations ("BaseMessage")
4. Subclass annotations (e.g. HumanMessage)
5. Postponed annotations (from __future__ import annotations)

In all these cases the per-message callable was misclassified as a
per-list counter, causing `AttributeError: 'list' object has no
attribute 'content'` at runtime.

The fix introduces `_is_per_message_token_counter()` which uses a
three-tier detection strategy:

1. Class annotation: `issubclass` instead of `is` (handles subclasses)
2. String annotation: match against known message class names (handles
   `from __future__ import annotations` and explicit string annotations)
3. No annotation: probe by calling with a dummy HumanMessage and
   catching TypeError/AttributeError (handles lambdas and un-annotated
   functions while correctly classifying `len` as a list counter)

Fixes langchain-ai#35629
@github-actions github-actions Bot added external core `langchain-core` package issues & PRs and removed external labels Mar 8, 2026
@codspeed-hq

codspeed-hq Bot commented Mar 8, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

✅ 13 untouched benchmarks
⏩ 23 skipped benchmarks1


Comparing MaxwellCalkin:fix/trim-messages-token-counter-detection (b098b67) with master (29134dc)2

Open in CodSpeed

Footnotes

  1. 23 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on master (532b014) during the generation of this report, so 29134dc was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@gautamvarmadatla

GAUTAM V DATLA (gautamvarmadatla) commented Mar 8, 2026

Copy link
Copy Markdown

Hi! Creator of the issue here

Thanks for working on this. However, I already have a PR open to fix it from when I originally opened the issue ( and there 4 other PRs too ) . Please try to avoid duplicates :)

@ccurme ccurme (ccurme) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Duplicated with #35630.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core `langchain-core` package issues & PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

trim_messages breaks when token_counter is a per-message callable (lambda, subclass annotation, or postponed annotations)

3 participants