litellm/tests/test_litellm/test_count_tokens_public_api.py
yassin b5a7032eb4 fix(proxy): run the remaining inline token counts off the event loop
Wrap the context-management editors, the end-of-stream chunk builder,
acount_tokens, the compression interception hook, the passthrough
interrupted-stream recovery, the A2A usage counters, and the semantic
cache embedding truncation in asyncify so a multi-megabyte payload no
longer stalls the worker's event loop while it is tokenized

The pass-through suite now drains the process-global logging worker
from an autouse conftest fixture so work queued on one test's loop
cannot fire against the next test's callbacks

Resolves LIT-7190

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-08 18:42:38 +00:00

177 lines
5.9 KiB
Python

"""
Tests for litellm.acount_tokens() public API.
"""
import asyncio
import os
from unittest.mock import AsyncMock, patch
import litellm
from litellm.types.utils import TokenCountResponse
def test_acount_tokens_routes_to_openai():
"""Test that acount_tokens routes to OpenAI token counter for openai/ models."""
with patch(
"litellm.llms.openai.responses.count_tokens.token_counter.openai_count_tokens_handler.handle_count_tokens_request",
new_callable=AsyncMock,
return_value={"input_tokens": 15},
):
result = asyncio.run(
litellm.acount_tokens(
model="openai/gpt-4o",
messages=[{"role": "user", "content": "Hello, how are you?"}],
api_key="sk-test-key",
)
)
assert result.total_tokens == 15
assert result.tokenizer_type == "openai_api"
assert result.request_model == "openai/gpt-4o"
def test_acount_tokens_routes_to_anthropic():
"""Test that acount_tokens routes to Anthropic token counter for anthropic/ models."""
with patch(
"litellm.llms.anthropic.count_tokens.token_counter.anthropic_count_tokens_handler.handle_count_tokens_request",
new_callable=AsyncMock,
return_value={"input_tokens": 20},
):
result = asyncio.run(
litellm.acount_tokens(
model="anthropic/claude-3-5-sonnet-20241022",
messages=[{"role": "user", "content": "Hello Claude!"}],
api_key="sk-ant-test-key",
)
)
assert result.total_tokens == 20
assert result.tokenizer_type == "anthropic_api"
assert result.request_model == "anthropic/claude-3-5-sonnet-20241022"
def test_acount_tokens_fallback_to_local():
"""Test that unsupported providers fall back to local tiktoken counting."""
result = asyncio.run(
litellm.acount_tokens(
model="together_ai/meta-llama/Llama-3-8b-chat-hf",
messages=[{"role": "user", "content": "Hello"}],
)
)
assert result.total_tokens > 0
assert result.tokenizer_type == "local_tokenizer"
def test_acount_tokens_with_tools():
"""Test that tools are passed through to the token counter."""
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather info",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
},
},
}
]
with patch(
"litellm.llms.openai.responses.count_tokens.token_counter.openai_count_tokens_handler.handle_count_tokens_request",
new_callable=AsyncMock,
return_value={"input_tokens": 30},
) as mock_handler:
result = asyncio.run(
litellm.acount_tokens(
model="openai/gpt-4o",
messages=[{"role": "user", "content": "What's the weather?"}],
tools=tools,
api_key="sk-test-key",
)
)
assert result.total_tokens == 30
mock_handler.assert_called_once()
call_kwargs = mock_handler.call_args
assert call_kwargs.kwargs.get("tools") == tools
def test_acount_tokens_with_system():
"""Test that system messages are passed through."""
with patch(
"litellm.llms.openai.responses.count_tokens.token_counter.openai_count_tokens_handler.handle_count_tokens_request",
new_callable=AsyncMock,
return_value={"input_tokens": 25},
):
result = asyncio.run(
litellm.acount_tokens(
model="openai/gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
system="You are a helpful assistant.",
api_key="sk-test-key",
)
)
assert result.total_tokens == 25
def test_acount_tokens_api_error_falls_back():
"""Test that API errors in token counting return error response."""
from litellm.llms.openai.common_utils import OpenAIError
with patch(
"litellm.llms.openai.responses.count_tokens.token_counter.openai_count_tokens_handler.handle_count_tokens_request",
new_callable=AsyncMock,
side_effect=OpenAIError(status_code=401, message="Invalid API key"),
):
result = asyncio.run(
litellm.acount_tokens(
model="openai/gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
api_key="sk-bad-key",
)
)
# Should fall back to local tokenizer when provider API errors
assert result.error is False
assert result.tokenizer_type == "local_tokenizer"
assert result.total_tokens > 0
def test_acount_tokens_no_api_key_falls_back(monkeypatch):
"""Test that missing API key falls back to local counting."""
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
result = asyncio.run(
litellm.acount_tokens(
model="openai/gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
)
# Should fall back to local tokenizer since no API key
assert result.total_tokens > 0
assert result.tokenizer_type == "local_tokenizer"
async def test_acount_tokens_local_fallback_counts_off_the_event_loop():
from tests.large_text import text
from tests.test_litellm.litellm_core_utils.event_loop_lag import (
assert_loop_stayed_free,
timed_with_loop_lags,
warm_tokenizer,
)
model = "together_ai/meta-llama/Llama-3-8b-chat-hf"
warm_tokenizer(model)
result, took, lags = await timed_with_loop_lags(
lambda: litellm.acount_tokens(model=model, messages=[{"role": "user", "content": text * 100}])
)
assert result.tokenizer_type == "local_tokenizer"
assert result.total_tokens > 100_000
assert_loop_stayed_free(took, lags)