"""Подсчёт токенов для чанкера и плагина `QwenLocal`.""" import logging from typing import Any logger = logging.getLogger(__name__) class QwenTokenCounter: """Подсчёт токенов токенизатором модели Qwen2.5-3B-Instruct. Загрузка `tokenizers.Tokenizer` — ленивая (при первом вызове), чтобы инстанцирование плагина в API-процессе не тянуло за собой файл токенизатора. Если файл отсутствует или повреждён — используется эвристический фолбэк `len(text) // 3`, чтобы пайплайн не падал из-за отсутствия токенизатора (например, в dev-окружении без volume модели). """ def __init__(self, tokenizer_path: str | None = None) -> None: self._tokenizer_path = tokenizer_path self._tokenizer: Any | None = None self._load_attempted = False def _ensure_loaded(self) -> None: """Загрузить токенизатор один раз (синглтон на инстанс counter'а).""" if self._load_attempted: return self._load_attempted = True if not self._tokenizer_path: logger.warning("Путь к токенизатору не задан, использую эвристику len(text) // 3") return try: from tokenizers import Tokenizer self._tokenizer = Tokenizer.from_file(self._tokenizer_path) except Exception: logger.warning( "Не удалось загрузить токенизатор из %s, использую эвристику len(text) // 3", self._tokenizer_path, ) self._tokenizer = None def __call__(self, text: str) -> int: """Вернуть число токенов в тексте (или эвристическую оценку).""" self._ensure_loaded() if self._tokenizer is not None: encoded: int = len(self._tokenizer.encode(text).ids) return encoded return len(text) // 3