Merge remote-tracking branch 'upstream/master' into integration-update

This commit is contained in:
pulvedu 2025-03-19 14:42:00 -04:00
commit 5d5b27f6ec
2 changed files with 206 additions and 0 deletions

View File

@ -12,6 +12,7 @@ from __future__ import annotations
import base64 import base64
import inspect import inspect
import json import json
import math
from collections.abc import Iterable, Sequence from collections.abc import Iterable, Sequence
from functools import partial from functools import partial
from typing import ( from typing import (
@ -1424,3 +1425,80 @@ def _convert_to_openai_tool_calls(tool_calls: list[ToolCall]) -> list[dict]:
} }
for tool_call in tool_calls for tool_call in tool_calls
] ]
def count_tokens_approximately(
messages: Iterable[MessageLikeRepresentation],
*,
chars_per_token: float = 4.0,
extra_tokens_per_message: float = 3.0,
count_name: bool = True,
) -> int:
"""Approximate the total number of tokens in messages.
The token count includes stringified message content, role, and (optionally) name.
- For AI messages, the token count also includes stringified tool calls.
- For tool messages, the token count also includes the tool call ID.
Args:
messages: List of messages to count tokens for.
chars_per_token: Number of characters per token to use for the approximation.
Default is 4 (one token corresponds to ~4 chars for common English text).
You can also specify float values for more fine-grained control.
See more here: https://platform.openai.com/tokenizer
extra_tokens_per_message: Number of extra tokens to add per message.
Default is 3 (special tokens, including beginning/end of message).
You can also specify float values for more fine-grained control.
See more here:
https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
count_name: Whether to include message names in the count.
Enabled by default.
Returns:
Approximate number of tokens in the messages.
Note:
This is a simple approximation that may not match the exact token count
used by specific models. For accurate counts, use model-specific tokenizers.
.. versionadded:: 0.3.46
"""
token_count = 0.0
for message in convert_to_messages(messages):
message_chars = 0
if isinstance(message.content, str):
message_chars += len(message.content)
# TODO: add support for approximate counting for image blocks
else:
content = repr(message.content)
message_chars += len(content)
if (
isinstance(message, AIMessage)
# exclude Anthropic format as tool calls are already included in the content
and not isinstance(message.content, list)
and message.tool_calls
):
tool_calls_content = repr(message.tool_calls)
message_chars += len(tool_calls_content)
if isinstance(message, ToolMessage):
message_chars += len(message.tool_call_id)
role = _get_message_openai_role(message)
message_chars += len(role)
if message.name and count_name:
message_chars += len(message.name)
# NOTE: we're rounding up per message to ensure that
# individual message token counts add up to the total count
# for a list of messages
token_count += math.ceil(message_chars / chars_per_token)
# add extra tokens per message
token_count += extra_tokens_per_message
# round up once more time in case extra_tokens_per_message is a float
return math.ceil(token_count)

View File

@ -18,6 +18,7 @@ from langchain_core.messages import (
from langchain_core.messages.utils import ( from langchain_core.messages.utils import (
convert_to_messages, convert_to_messages,
convert_to_openai_messages, convert_to_openai_messages,
count_tokens_approximately,
filter_messages, filter_messages,
merge_message_runs, merge_message_runs,
trim_messages, trim_messages,
@ -976,3 +977,130 @@ def test_convert_to_openai_messages_developer() -> None:
] ]
result = convert_to_openai_messages(messages) result = convert_to_openai_messages(messages)
assert result == [{"role": "developer", "content": "a"}] * 2 assert result == [{"role": "developer", "content": "a"}] * 2
def test_count_tokens_approximately_empty_messages() -> None:
# Test with empty message list
assert count_tokens_approximately([]) == 0
# Test with empty content
messages = [HumanMessage(content="")]
# 4 role chars -> 1 + 3 = 4 tokens
assert count_tokens_approximately(messages) == 4
def test_count_tokens_approximately_with_names() -> None:
messages = [
# 5 chars + 4 role chars -> 3 + 3 = 6 tokens
# (with name: extra 4 name chars, so total = 4 + 3 = 7 tokens)
HumanMessage(content="Hello", name="user"),
# 8 chars + 9 role chars -> 5 + 3 = 8 tokens
# (with name: extra 9 name chars, so total = 7 + 3 = 10 tokens)
AIMessage(content="Hi there", name="assistant"),
]
# With names included (default)
assert count_tokens_approximately(messages) == 17
# Without names
without_names = count_tokens_approximately(messages, count_name=False)
assert without_names == 14
def test_count_tokens_approximately_openai_format() -> None:
# same as test_count_tokens_approximately_with_names, but in OpenAI format
messages = [
{"role": "user", "content": "Hello", "name": "user"},
{"role": "assistant", "content": "Hi there", "name": "assistant"},
]
# With names included (default)
assert count_tokens_approximately(messages) == 17
# Without names
without_names = count_tokens_approximately(messages, count_name=False)
assert without_names == 14
def test_count_tokens_approximately_string_content() -> None:
messages = [
# 5 chars + 4 role chars -> 3 + 3 = 6 tokens
HumanMessage(content="Hello"),
# 8 chars + 9 role chars -> 5 + 3 = 8 tokens
AIMessage(content="Hi there"),
# 12 chars + 4 role chars -> 4 + 3 = 7 tokens
HumanMessage(content="How are you?"),
]
assert count_tokens_approximately(messages) == 21
def test_count_tokens_approximately_list_content() -> None:
messages = [
# '[{"foo": "bar"}]' -> 16 chars + 4 role chars -> 5 + 3 = 8 tokens
HumanMessage(content=[{"foo": "bar"}]),
# '[{"test": 123}]' -> 15 chars + 9 role chars -> 6 + 3 = 9 tokens
AIMessage(content=[{"test": 123}]),
]
assert count_tokens_approximately(messages) == 17
def test_count_tokens_approximately_tool_calls() -> None:
tool_calls = [{"name": "test_tool", "args": {"foo": "bar"}, "id": "1"}]
messages = [
# tool calls json -> 79 chars + 9 role chars -> 22 + 3 = 25 tokens
AIMessage(content="", tool_calls=tool_calls),
# 15 chars + 4 role chars -> 5 + 3 = 8 tokens
HumanMessage(content="Regular message"),
]
assert count_tokens_approximately(messages) == 33
# AI message w/ both content and tool calls
# 94 chars + 9 role chars -> 26 + 3 = 29 tokens
messages = [
AIMessage(content="Regular message", tool_calls=tool_calls),
]
assert count_tokens_approximately(messages) == 29
def test_count_tokens_approximately_custom_token_length() -> None:
messages = [
# 11 chars + 4 role chars -> (4 tokens of length 4 / 8 tokens of length 2) + 3
HumanMessage(content="Hello world"),
# 7 chars + 9 role chars -> (4 tokens of length 4 / 8 tokens of length 2) + 3
AIMessage(content="Testing"),
]
assert count_tokens_approximately(messages, chars_per_token=4) == 14
assert count_tokens_approximately(messages, chars_per_token=2) == 22
def test_count_tokens_approximately_large_message_content() -> None:
# Test with large content to ensure no issues
large_text = "x" * 10000
messages = [HumanMessage(content=large_text)]
# 10,000 chars + 4 role chars -> 2501 + 3 = 2504 tokens
assert count_tokens_approximately(messages) == 2504
def test_count_tokens_approximately_large_number_of_messages() -> None:
# Test with large content to ensure no issues
messages = [HumanMessage(content="x")] * 1_000
# 1 chars + 4 role chars -> 2 + 3 = 5 tokens
assert count_tokens_approximately(messages) == 5_000
def test_count_tokens_approximately_mixed_content_types() -> None:
# Test with a variety of content types in the same message list
tool_calls = [{"name": "test_tool", "args": {"foo": "bar"}, "id": "1"}]
messages = [
# 13 chars + 6 role chars -> 5 + 3 = 8 tokens
SystemMessage(content="System prompt"),
# '[{"foo": "bar"}]' -> 16 chars + 4 role chars -> 5 + 3 = 8 tokens
HumanMessage(content=[{"foo": "bar"}]),
# tool calls json -> 79 chars + 9 role chars -> 22 + 3 = 25 tokens
AIMessage(content="", tool_calls=tool_calls),
# 13 chars + 4 role chars + 9 name chars + 1 tool call ID char ->
# 7 + 3 = 10 tokens
ToolMessage(content="Tool response", name="test_tool", tool_call_id="1"),
]
token_count = count_tokens_approximately(messages)
assert token_count == 51
# Ensure that count is consistent if we do one message at a time
assert sum(count_tokens_approximately([m]) for m in messages) == token_count