Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-llm/src/lexigram/ai/llm/pricing/registry.py: 64%
64 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
1"""Token counter registry for managing model-to-counter mappings."""
3from __future__ import annotations
5import re
7from lexigram.contracts.ai.llm import TokenCounterProtocol
8from lexigram.logging import (
9 get_logger,
10)
12logger = get_logger(__name__)
15def _tiktoken_available() -> bool:
16 """Check if tiktoken is installed."""
17 try:
18 import tiktoken # noqa: F401
20 return True
21 except ImportError:
22 return False
25def _transformers_available() -> bool:
26 """Check if HuggingFace transformers is installed."""
27 try:
28 import transformers # noqa: F401
30 return True
31 except ImportError:
32 return False
35def _mistral_available() -> bool:
36 """Check if mistral-common is installed."""
37 try:
38 import mistral_common # noqa: F401
40 return True
41 except ImportError:
42 return False
45class TokenCounterRegistry:
46 """Registry mapping model-name patterns to TokenCounterProtocol backends.
48 Uses named backend keys and regex patterns for flexible model mapping.
50 Usage::
52 registry = TokenCounterRegistry.with_defaults()
53 counter = registry.for_model("gpt-4o")
54 tokens = counter.count("Hello!")
55 """
57 def __init__(self) -> None:
58 """Create an empty registry."""
59 self._backends: dict[str, TokenCounterProtocol] = {}
60 self._patterns: list[tuple[re.Pattern[str], str]] = []
62 @classmethod
63 def with_defaults(cls) -> TokenCounterRegistry:
64 """Create registry with all available tokenizer backends.
66 Registers:
67 - char_estimate (always available, fallback)
68 - tiktoken (if installed, for OpenAI/Anthropic models)
69 - huggingface (if installed, for HuggingFace models)
70 - mistral (if installed, for Mistral models)
72 Returns:
73 TokenCounterRegistry pre-populated with default backends.
74 """
75 from lexigram.ai.llm.pricing.tokens import CharEstimateCounter
77 registry = cls()
78 registry.register("char_estimate", CharEstimateCounter()) # type: ignore[arg-type]
80 if _tiktoken_available():
81 from lexigram.ai.llm.pricing.tokens import TiktokenCounter
83 registry.register("tiktoken", TiktokenCounter()) # type: ignore[arg-type]
84 registry.map_models(r"gpt-.*|o[0-9].*|text-embedding-.*", "tiktoken")
85 logger.debug("token_counter_registry_tiktoken_registered")
86 else:
87 logger.warning(
88 "token_counter_registry_tiktoken_unavailable",
89 fallback="char_estimate",
90 )
92 if _transformers_available():
93 from lexigram.ai.llm.pricing.tokens import HuggingFaceCounter
95 registry.register("huggingface", HuggingFaceCounter()) # type: ignore[arg-type]
96 registry.map_models(
97 r"llama-.*|qwen-.*|deepseek-.*|gemma-.*",
98 "huggingface",
99 )
100 logger.debug("token_counter_registry_huggingface_registered")
102 if _mistral_available():
103 from lexigram.ai.llm.pricing.tokens import MistralCounter
105 registry.register("mistral", MistralCounter()) # type: ignore[arg-type]
106 registry.map_models(r"mistral-.*|codestral-.*", "mistral")
107 logger.debug("token_counter_registry_mistral_registered")
109 return registry
111 def register(self, key: str, counter: TokenCounterProtocol) -> None:
112 """Register a counter backend under a named key.
114 Args:
115 key: Backend name (e.g., 'tiktoken', 'huggingface', 'char_estimate').
116 counter: Counter implementing TokenCounterProtocol.
117 """
118 self._backends[key] = counter
120 def map_models(self, pattern: str, counter_key: str) -> None:
121 """Map a regex pattern of model names to a backend key.
123 Args:
124 pattern: Regex pattern matching model names (case-insensitive).
125 counter_key: Backend key (must be registered).
126 """
127 if counter_key not in self._backends:
128 logger.warning("token_counter_map_key_not_found", key=counter_key)
129 return
130 self._patterns.append((re.compile(pattern, re.IGNORECASE), counter_key))
132 def for_model(self, model: str) -> TokenCounterProtocol:
133 """Get the best counter for the given model name.
135 Tries exact regex match in _patterns first, falls back to 'char_estimate'.
137 Args:
138 model: Model name.
140 Returns:
141 TokenCounterProtocol implementation.
142 """
143 for compiled_pattern, key in self._patterns:
144 if compiled_pattern.match(model):
145 return self._backends[key]
146 if "char_estimate" in self._backends:
147 return self._backends["char_estimate"]
148 from lexigram.ai.llm.pricing.tokens import CharEstimateCounter
150 return CharEstimateCounter() # type: ignore[return-value]