fix(token-counter): count reasoning summaries in message content

This commit is contained in:
Mingyang Wu 2026-09-11 17:50:09 +08:00
parent 9a715df212
commit ec3368bcd5
2 changed files with 80 additions and 1 deletions

View file

@ -46,6 +46,7 @@ from litellm.types.llms.openai import (
AllMessageValues,
ChatCompletionDocumentObject,
ChatCompletionNamedToolChoiceParam,
ChatCompletionReasoningItem,
ChatCompletionToolParam,
OpenAIMessageContentListBlock,
)
@ -846,6 +847,7 @@ def _count_content_list(
| AnthropicMessagesTextParam
| AnthropicMessagesImageParam
| AnthropicMessagesDocumentParam
| ChatCompletionReasoningItem
],
use_default_image_token_count: bool,
default_token_count: int | None,
@ -893,6 +895,10 @@ def _count_content_list(
thinking_text = str(c.get("thinking", ""))
if thinking_text:
num_tokens += count_function(thinking_text)
elif c["type"] == "reasoning":
num_tokens += sum(
count_function(text) for summary in c.get("summary", ()) if (text := summary.get("text"))
)
elif c["type"] == "tool_reference":
# Anthropic tool-search reference block: a lightweight pointer to
# a deferred tool, e.g. {"type": "tool_reference", "tool_name": ...}.
@ -909,7 +915,7 @@ def _count_content_list(
raise ValueError(
f"Invalid content item type: {content_type}. "
f"Expected str or dict with 'type' field "
f"(text, image_url, image, document, file, tool_use, tool_result, thinking, tool_reference)."
f"(text, image_url, image, document, file, tool_use, tool_result, thinking, reasoning, tool_reference)."
)
return num_tokens
except Exception as e:

View file

@ -7,6 +7,7 @@ import threading
import time
import traceback
from concurrent.futures import Future, wait
from copy import deepcopy
from typing import Final
from unittest.mock import MagicMock
@ -31,6 +32,7 @@ from litellm.litellm_core_utils.token_counter import (
offload_token_count,
)
from litellm.litellm_core_utils.token_counter import token_counter as token_counter_new
from litellm.types.llms.openai import ChatCompletionReasoningItem
from tests.large_text import text
from tests.test_litellm.litellm_core_utils.event_loop_lag import (
assert_loop_stayed_free,
@ -1250,6 +1252,77 @@ def test_token_counter_with_thinking_content():
), f"Expected minimal token count for empty thinking block, got {tokens_no_thinking}"
@pytest.mark.parametrize("model", ["gemini/gemini-3.8-flash", "gpt-6-astra"])
@pytest.mark.parametrize(
("reasoning", "summary_texts"),
[
({"type": "reasoning"}, ()),
({"type": "reasoning", "summary": []}, ()),
({"type": "reasoning", "encrypted_content": "opaque " * 1000}, ()),
(
{"type": "reasoning", "summary": [{"type": "summary_text", "text": "A short synthetic summary."}]},
("A short synthetic summary.",),
),
(
{
"type": "reasoning",
"summary": [
{"type": "summary_text", "text": "First thought."},
{"type": "summary_text", "text": ""},
{"type": "summary_text", "text": "Second thought."},
],
"id": "rs_opaque_identifier",
"encrypted_content": "opaque " * 1000,
},
("First thought.", "", "Second thought."),
),
],
ids=["missing-summary", "empty-summary", "encrypted-only", "summary", "multiple-summaries-with-metadata"],
)
def test_token_counter_with_reasoning_content(
model: str, reasoning: ChatCompletionReasoningItem, summary_texts: tuple[str, ...]
) -> None:
original: Final = deepcopy(reasoning)
messages: Final = [
{"role": "user", "content": "Context marker."},
{"role": "assistant", "content": [reasoning, {"type": "text", "text": "Ready."}]},
]
equivalent_messages: Final = [
{"role": "user", "content": "Context marker."},
{
"role": "assistant",
"content": [
*({"type": "text", "text": value} for value in summary_texts),
{"type": "text", "text": "Ready."},
],
},
]
assert token_counter(model=model, messages=messages) == token_counter(model=model, messages=equivalent_messages)
assert reasoning == original
def test_reasoning_content_preserves_prompt_cache_eligibility() -> None:
model: Final = "gemini/gemini-3.8-flash"
messages: Final = [
{"role": "user", "content": "Context marker. " * 1800},
{
"role": "assistant",
"content": [
{"type": "reasoning", "summary": [{"type": "summary_text", "text": "A short synthetic summary."}]},
{"type": "text", "text": "Ready."},
],
},
{"role": "user", "content": "Reply with exactly OK."},
]
count: Final = token_counter(model=model, messages=messages)
assert count >= litellm.utils.get_prompt_cache_min_tokens(model)
assert litellm.utils.is_prompt_caching_valid_prompt(model=model, messages=messages)
assert litellm.utils.is_prompt_caching_valid_prompt(model=model, messages=messages, min_token_count=count)
assert not litellm.utils.is_prompt_caching_valid_prompt(model=model, messages=messages, min_token_count=count + 1)
def test_token_counter_with_tool_reference_block():
"""
Regression test: a message containing an Anthropic tool-search