fix(proxy): run prompt caching counts and custom tokenizer loads off the event loop

This commit is contained in:
mateo-berri 2026-09-07 22:21:45 -07:00
parent 83d500bac9
commit d202885f8b
7 changed files with 111 additions and 18 deletions

View file

@ -12713,7 +12713,9 @@ async def token_counter(request: TokenCountRequest, call_endpoint: bool = False)
CustomHuggingfaceTokenizer | None,
model_info.get("custom_tokenizer", None),
)
_tokenizer_used: Final = litellm.utils._select_tokenizer(model=model_to_use, custom_tokenizer=custom_tokenizer)
_tokenizer_used: Final = await asyncify(litellm.utils._select_tokenizer)(
model=model_to_use, custom_tokenizer=custom_tokenizer
)
tokenizer_used: Final = str(_tokenizer_used["type"])
system_message: Final = _system_message(system)

View file

@ -14,6 +14,7 @@ from litellm.integrations.anthropic_cache_control_hook import (
AnthropicCacheControlHook,
)
from litellm.integrations.custom_logger import CustomLogger, Span
from litellm.litellm_core_utils.asyncify import asyncify
from litellm.types.llms.openai import AllMessageValues
from litellm.types.utils import CallTypes, StandardLoggingPayload
from litellm.utils import get_prompt_cache_min_tokens, is_prompt_caching_valid_prompt
@ -61,7 +62,7 @@ class PromptCachingDeploymentCheck(CustomLogger):
if request_kwargs is not None and request_kwargs.get("_target_order") is not None:
return healthy_deployments
if messages is not None and is_prompt_caching_valid_prompt(
if messages is not None and await asyncify(is_prompt_caching_valid_prompt)(
messages=messages,
model=model,
min_token_count=_get_min_token_count_for_deployments(healthy_deployments),
@ -139,7 +140,7 @@ class PromptCachingDeploymentCheck(CustomLogger):
return
## PROMPT CACHING - cache model id, if prompt caching valid prompt + provider
if is_prompt_caching_valid_prompt(
if await asyncify(is_prompt_caching_valid_prompt)(
model=model,
messages=cast(list[AllMessageValues], messages),
):

View file

@ -2284,15 +2284,7 @@ def create_pretrained_tokenizer(identifier: str, revision="main", auth_token: st
dict: A dictionary with the tokenizer and its type.
"""
try:
tokenizer = Tokenizer.from_pretrained(
identifier,
revision=revision,
auth_token=auth_token,
)
except Exception as e:
verbose_logger.error("Error creating pretrained tokenizer: %s. Defaulting to version without 'auth_token'.", e)
tokenizer = Tokenizer.from_pretrained(identifier, revision=revision)
tokenizer: Final = Tokenizer.from_pretrained(identifier, revision=revision, token=auth_token)
return {"type": "huggingface_tokenizer", "tokenizer": tokenizer}

View file

@ -23,9 +23,9 @@ from litellm.proxy.proxy_server import token_counter
def _fake_hf_tokenizer(num_tokens: int) -> MagicMock:
encoding = MagicMock()
encoding.ids = list(range(num_tokens))
encoding.__len__.return_value = num_tokens
tokenizer = MagicMock()
tokenizer.encode.return_value = encoding
tokenizer.encode_batch_fast.return_value = [encoding]
return tokenizer
@ -68,13 +68,11 @@ async def test_custom_tokenizer_from_model_info_is_used(monkeypatch):
)
)
mock_tokenizer_cls.from_pretrained.assert_called_once_with(
"my-org/custom-tokenizer", revision="v2", auth_token=None
)
mock_tokenizer_cls.from_pretrained.assert_called_once_with("my-org/custom-tokenizer", revision="v2", token=None)
assert response.tokenizer_type == "huggingface_tokenizer"
assert response.request_model == "my-embedding-model"
assert response.model_used == "self-hosted-embedder"
assert response.total_tokens > 0
assert response.total_tokens >= 7
@pytest.mark.asyncio

View file

@ -25,6 +25,7 @@ async def timed_with_loop_lags(run: Callable[[], Awaitable[T]]) -> tuple[T, floa
finished: Final = asyncio.Event()
async def timed() -> tuple[T, float]:
await asyncio.sleep(0)
started: Final = time.perf_counter()
try:
return await run(), time.perf_counter() - started

View file

@ -12877,3 +12877,40 @@ async def test_token_counter_keeps_the_event_loop_free_during_a_huggingface_coun
assert response.total_tokens > 0
assert_loop_stayed_free(took, lags)
async def test_token_counter_loads_a_custom_tokenizer_off_the_event_loop(monkeypatch):
from tokenizers import Tokenizer
from litellm import Router
from tests.test_litellm.litellm_core_utils.event_loop_lag import assert_loop_stayed_free, timed_with_loop_lags
claude_tokenizer: Final = litellm.utils._select_tokenizer("claude-fable-5")["tokenizer"]
class SlowHubTokenizer:
@staticmethod
def from_pretrained(identifier: str, revision: str = "main", token: str | None = None) -> Tokenizer:
time.sleep(0.3)
return claude_tokenizer
monkeypatch.setattr(litellm.utils, "Tokenizer", SlowHubTokenizer)
monkeypatch.setattr(
"litellm.proxy.proxy_server.llm_router",
Router(
model_list=[
{
"model_name": "self-hosted",
"litellm_params": {"model": "openai/self-hosted-model", "api_base": "http://localhost:8080/v1"},
"model_info": {"custom_tokenizer": {"identifier": "my-org/tokenizer", "revision": "main", "auth_token": None}},
}
]
),
)
response, took, lags = await timed_with_loop_lags(
lambda: proxy_server_module.token_counter(TokenCountRequest(model="self-hosted", prompt="count me off the loop"))
)
assert response.tokenizer_type == "huggingface_tokenizer"
assert response.total_tokens > 0
assert_loop_stayed_free(took, lags)

View file

@ -477,3 +477,65 @@ async def test_wildcard_route_resolves_underlying_model_minimum(local_model_cost
assert deployments[0]["litellm_params"]["model"] == "anthropic/claude-opus-4-6"
assert _get_min_token_count_for_deployments(deployments) == 4096
@pytest.mark.asyncio
async def test_async_filter_deployments_counts_the_prompt_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,
)
warm_tokenizer("anthropic/claude-fable-5")
check = PromptCachingDeploymentCheck(cache=DualCache())
deployments = _deployments("anthropic/claude-fable-5")
messages = cast(List[AllMessageValues], [{"role": "user", "content": text * 100}])
result, took, lags = await timed_with_loop_lags(
lambda: check.async_filter_deployments(
model=MODEL_GROUP_ALIAS, healthy_deployments=deployments, messages=messages
)
)
assert result == deployments
assert_loop_stayed_free(took, lags)
@pytest.mark.asyncio
async def test_async_log_success_event_counts_the_prompt_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,
)
warm_tokenizer("anthropic/claude-fable-5")
cache = DualCache()
check = PromptCachingDeploymentCheck(cache=cache)
messages = cast(
List[AllMessageValues],
[{"role": "user", "content": [{"type": "text", "text": text * 100, "cache_control": {"type": "ephemeral"}}]}],
)
standard_logging_object = {
"call_type": "acompletion",
"model": "anthropic/claude-fable-5",
"messages": messages,
"model_id": "dep-1",
}
_, took, lags = await timed_with_loop_lags(
lambda: check.async_log_success_event(
kwargs={"standard_logging_object": standard_logging_object},
response_obj=None,
start_time=None,
end_time=None,
)
)
assert await PromptCachingCache(cache=cache).async_get_model_id(messages=messages, tools=None) == {
"model_id": "dep-1"
}
assert_loop_stayed_free(took, lags)